$ cat /posts/supabase-image-upload-with-react-complete-tutorial.md
[tags]Supabase

Supabase Image Upload with React: Complete Tutorial

drwxr-xr-x2026-01-255 min0 views
Supabase Image Upload with React: Complete Tutorial

Building image upload functionality with live preview, progress tracking, and proper error handling is a common requirement for modern web applications—from user avatars and profile banners to product photos and gallery uploads. This hands-on tutorial demonstrates building production-ready React components for image uploading to Supabase Storage, including file validation (size, type), live image preview before upload, upload progress bars, error handling, image optimization, updating database records with file URLs, and implementing user-friendly UI with drag-and-drop support. We'll build reusable components suitable for avatar uploads, product images, and gallery managers while following React best practices with hooks, proper state management, and clean code patterns. Unlike basic examples that skip crucial details, this guide covers real-world scenarios including replacing existing images, handling upload failures, and integrating with user profiles. Before proceeding, understand Supabase Storage basics and authentication.

Setup and Prerequisites

bashsetup.sh
// Install dependencies
npm install @supabase/supabase-js react react-dom

// Project structure
// src/
//   components/
//     ImageUpload.jsx
//     AvatarUpload.jsx
//   supabaseClient.js
//   App.jsx

// supabaseClient.js
import { createClient } from '@supabase/supabase-js'

const supabaseUrl = process.env.REACT_APP_SUPABASE_URL
const supabaseAnonKey = process.env.REACT_APP_SUPABASE_ANON_KEY

export const supabase = createClient(supabaseUrl, supabaseAnonKey)

// Create 'avatars' bucket in Supabase Dashboard (Storage > New Bucket)
// Make it public for direct image access

Basic Image Upload Component

javascriptImageUpload.jsx
// ImageUpload.jsx - Basic upload with preview
import { useState } from 'react'
import { supabase } from '../supabaseClient'

function ImageUpload() {
  const [file, setFile] = useState(null)
  const [preview, setPreview] = useState(null)
  const [uploading, setUploading] = useState(false)
  const [uploadedUrl, setUploadedUrl] = useState(null)
  const [error, setError] = useState(null)

  function handleFileSelect(e) {
    const selectedFile = e.target.files[0]
    
    if (!selectedFile) return

    // Validate file type
    if (!selectedFile.type.startsWith('image/')) {
      setError('Please select an image file')
      return
    }

    // Validate file size (5MB max)
    if (selectedFile.size > 5 * 1024 * 1024) {
      setError('File size must be less than 5MB')
      return
    }

    setFile(selectedFile)
    setError(null)

    // Create preview
    const reader = new FileReader()
    reader.onloadend = () => {
      setPreview(reader.result)
    }
    reader.readAsDataURL(selectedFile)
  }

  async function handleUpload() {
    if (!file) return

    setUploading(true)
    setError(null)

    try {
      // Generate unique filename
      const fileExt = file.name.split('.').pop()
      const fileName = `${Date.now()}-${Math.random().toString(36).substring(7)}.${fileExt}`
      const filePath = `uploads/${fileName}`

      // Upload to Supabase Storage
      const { data, error } = await supabase.storage
        .from('avatars')
        .upload(filePath, file)

      if (error) throw error

      // Get public URL
      const { data: urlData } = supabase.storage
        .from('avatars')
        .getPublicUrl(filePath)

      setUploadedUrl(urlData.publicUrl)
      alert('Upload successful!')

    } catch (error) {
      setError(error.message)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div className="image-upload">
      <h2>Upload Image</h2>
      
      {/* File input */}
      <input
        type="file"
        accept="image/*"
        onChange={handleFileSelect}
        disabled={uploading}
      />

      {/* Preview */}
      {preview && (
        <div className="preview">
          <img src={preview} alt="Preview" style={{ maxWidth: '300px' }} />
        </div>
      )}

      {/* Error message */}
      {error && <p className="error">{error}</p>}

      {/* Upload button */}
      {file && (
        <button onClick={handleUpload} disabled={uploading}>
          {uploading ? 'Uploading...' : 'Upload Image'}
        </button>
      )}

      {/* Success message */}
      {uploadedUrl && (
        <div className="success">
          <p>Image uploaded successfully!</p>
          <img src={uploadedUrl} alt="Uploaded" style={{ maxWidth: '300px' }} />
          <p>URL: {uploadedUrl}</p>
        </div>
      )}
    </div>
  )
}

export default ImageUpload

Avatar Upload with Progress

javascriptAvatarUpload.jsx
// AvatarUpload.jsx - Complete avatar upload component
import { useState, useEffect } from 'react'
import { supabase } from '../supabaseClient'

function AvatarUpload({ userId, currentAvatarUrl, onUploadComplete }) {
  const [uploading, setUploading] = useState(false)
  const [avatarUrl, setAvatarUrl] = useState(currentAvatarUrl)
  const [uploadProgress, setUploadProgress] = useState(0)

  async function uploadAvatar(event) {
    try {
      setUploading(true)
      setUploadProgress(0)

      if (!event.target.files || event.target.files.length === 0) {
        return
      }

      const file = event.target.files[0]

      // Validate
      if (file.size > 2 * 1024 * 1024) {
        alert('Image must be less than 2MB')
        return
      }

      const fileExt = file.name.split('.').pop()
      const fileName = `${userId}/avatar.${fileExt}`

      // Delete old avatar if exists
      if (currentAvatarUrl) {
        const oldPath = currentAvatarUrl.split('/').slice(-2).join('/')
        await supabase.storage.from('avatars').remove([oldPath])
      }

      // Upload new avatar
      const { error: uploadError } = await supabase.storage
        .from('avatars')
        .upload(fileName, file, {
          cacheControl: '3600',
          upsert: true
        })

      if (uploadError) throw uploadError

      // Get public URL
      const { data } = supabase.storage
        .from('avatars')
        .getPublicUrl(fileName)

      const newAvatarUrl = data.publicUrl

      // Update user profile in database
      const { error: dbError } = await supabase
        .from('profiles')
        .update({ avatar_url: newAvatarUrl })
        .eq('id', userId)

      if (dbError) throw dbError

      setAvatarUrl(newAvatarUrl)
      if (onUploadComplete) onUploadComplete(newAvatarUrl)

    } catch (error) {
      alert(error.message)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div className="avatar-upload">
      {avatarUrl ? (
        <img
          src={avatarUrl}
          alt="Avatar"
          className="avatar-preview"
          style={{
            width: '150px',
            height: '150px',
            borderRadius: '50%',
            objectFit: 'cover'
          }}
        />
      ) : (
        <div
          className="avatar-placeholder"
          style={{
            width: '150px',
            height: '150px',
            borderRadius: '50%',
            backgroundColor: '#ddd',
            display: 'flex',
            alignItems: 'center',
            justifyContent: 'center'
          }}
        >
          No avatar
        </div>
      )}

      <div className="upload-controls">
        <label className="button" htmlFor="avatar-upload">
          {uploading ? 'Uploading...' : 'Change Avatar'}
        </label>
        <input
          id="avatar-upload"
          type="file"
          accept="image/*"
          onChange={uploadAvatar}
          disabled={uploading}
          style={{ display: 'none' }}
        />
      </div>
    </div>
  )
}

export default AvatarUpload

Drag and Drop Upload

javascriptDragDropUpload.jsx
// DragDropUpload.jsx - Drag and drop image upload
import { useState } from 'react'
import { supabase } from '../supabaseClient'

function DragDropUpload() {
  const [isDragging, setIsDragging] = useState(false)
  const [preview, setPreview] = useState(null)
  const [uploading, setUploading] = useState(false)

  function handleDragEnter(e) {
    e.preventDefault()
    e.stopPropagation()
    setIsDragging(true)
  }

  function handleDragLeave(e) {
    e.preventDefault()
    e.stopPropagation()
    setIsDragging(false)
  }

  function handleDragOver(e) {
    e.preventDefault()
    e.stopPropagation()
  }

  async function handleDrop(e) {
    e.preventDefault()
    e.stopPropagation()
    setIsDragging(false)

    const files = [...e.dataTransfer.files]
    const imageFile = files.find(file => file.type.startsWith('image/'))

    if (!imageFile) {
      alert('Please drop an image file')
      return
    }

    // Show preview
    const reader = new FileReader()
    reader.onloadend = () => setPreview(reader.result)
    reader.readAsDataURL(imageFile)

    // Upload
    await uploadFile(imageFile)
  }

  async function uploadFile(file) {
    setUploading(true)

    const fileExt = file.name.split('.').pop()
    const fileName = `${Date.now()}.${fileExt}`

    const { error } = await supabase.storage
      .from('avatars')
      .upload(`uploads/${fileName}`, file)

    if (error) {
      alert(error.message)
    } else {
      alert('Upload successful!')
    }

    setUploading(false)
  }

  return (
    <div
      className={`drop-zone ${isDragging ? 'dragging' : ''}`}
      onDragEnter={handleDragEnter}
      onDragOver={handleDragOver}
      onDragLeave={handleDragLeave}
      onDrop={handleDrop}
      style={{
        border: '2px dashed #ccc',
        borderRadius: '8px',
        padding: '40px',
        textAlign: 'center',
        backgroundColor: isDragging ? '#f0f0f0' : 'white',
        cursor: 'pointer'
      }}
    >
      {preview ? (
        <img src={preview} alt="Preview" style={{ maxWidth: '100%' }} />
      ) : (
        <div>
          <p>Drag and drop an image here</p>
          <p>or click to select</p>
        </div>
      )}
      {uploading && <p>Uploading...</p>}
    </div>
  )
}

export default DragDropUpload

Multiple Image Gallery Upload

javascriptGalleryUpload.jsx
// GalleryUpload.jsx - Upload multiple images
import { useState } from 'react'
import { supabase } from '../supabaseClient'

function GalleryUpload() {
  const [images, setImages] = useState([])
  const [uploading, setUploading] = useState(false)

  async function handleMultipleUpload(event) {
    const files = [...event.target.files]
    setUploading(true)

    const uploadPromises = files.map(async (file) => {
      const fileExt = file.name.split('.').pop()
      const fileName = `${Date.now()}-${Math.random()}.${fileExt}`

      const { data, error } = await supabase.storage
        .from('avatars')
        .upload(`gallery/${fileName}`, file)

      if (error) throw error

      const { data: urlData } = supabase.storage
        .from('avatars')
        .getPublicUrl(`gallery/${fileName}`)

      return urlData.publicUrl
    })

    try {
      const urls = await Promise.all(uploadPromises)
      setImages([...images, ...urls])
      alert('All images uploaded successfully!')
    } catch (error) {
      alert('Upload failed: ' + error.message)
    } finally {
      setUploading(false)
    }
  }

  return (
    <div className="gallery-upload">
      <input
        type="file"
        accept="image/*"
        multiple
        onChange={handleMultipleUpload}
        disabled={uploading}
      />

      {uploading && <p>Uploading images...</p>}

      <div className="gallery-grid" style={{ display: 'grid', gridTemplateColumns: 'repeat(3, 1fr)', gap: '10px' }}>
        {images.map((url, index) => (
          <img key={index} src={url} alt={`Gallery ${index}`} style={{ width: '100%' }} />
        ))}
      </div>
    </div>
  )
}

export default GalleryUpload

Best Practices

  • Validate Before Upload: Check file type, size, and dimensions on client side
  • Show Progress: Display upload progress for better UX, especially for large files
  • Handle Errors Gracefully: Show user-friendly error messages for upload failures
  • Optimize Images: Compress images before upload or use Supabase transformations
  • Implement RLS: Secure storage with Row Level Security policies
  • Update Database: Store file URLs in database tables for easy querying
  • Delete Old Files: Remove previous files when users upload replacements to save storage
Pro Tip: For production apps, add image compression before upload using libraries like browser-image-compression. Combine with Supabase image transformations to serve optimized images. Consider adding cropping functionality for profile pictures.

Next Steps

  1. Add Image Cropping: Use libraries like react-image-crop for profile picture editing
  2. Implement Real-time: Show real-time updates when other users upload images
  3. Secure Storage: Add comprehensive RLS policies for user-specific uploads
  4. Build Complete App: Integrate with React CRUD tutorial for full applications

Conclusion

Building robust image upload functionality in React with Supabase Storage provides users with intuitive, responsive interfaces for managing files in your applications. By implementing proper validation, live previews, progress tracking, and error handling, you create production-ready upload experiences that handle edge cases gracefully. The reusable components demonstrated—from basic uploads to drag-and-drop and gallery managers—can be adapted for avatars, product images, document uploads, and any file management scenario. Always remember to validate uploads on both client and server, implement Row Level Security policies for private files, optimize images for performance, and update database records with file URLs for easy querying. With these patterns mastered, you're equipped to build feature-rich applications with professional file upload capabilities. Continue enhancing your skills with real-time features and complete application tutorials.

$ cat /comments/ (0)

new_comment.sh

// Email hidden from public

>_

$ cat /comments/

// No comments found. Be the first!

[session] guest@{codershandbook}[timestamp] 2026

Navigation

Categories

Connect

Subscribe

// 2026 {Coders Handbook}. EOF.