2025-03-17 Web Development

Building a React Image Uploader with Supabase Storage

By O. Wolfson

Introduction

This component handles image uploading in a React application. It supports uploading multiple files or external image URLs. Each image can have an optional caption, and users can reorder images via drag-and-drop. Files are uploaded to Supabase Storage and displayed using their public URLs.

The functionality is structured to allow users to upload, sort, and annotate images efficiently within a single interface.

Key Features

  • Upload multiple image files (JPG, PNG, WEBP)
  • Drag-and-drop reordering
  • Optional captions per image
  • External image URL input
  • File validation (max 5MB, allowed types)
  • Supabase Storage integration
  • Remove image functionality

Setting Up the Project

bash
npx create-next-app@latest image-uploader
cd image-uploader
npm install @supabase/supabase-js uuid @dnd-kit/core @dnd-kit/sortable lucide-react

Connecting to Supabase

Create a Supabase client instance:

ts
import { createClient } from "@supabase/supabase-js";

const supabaseUrl = "https://your-supabase-url.supabase.co";
const supabaseKey = "your-supabase-anon-key";
const supabase = createClient(supabaseUrl, supabaseKey);

Component State

ts
const [uploading, setUploading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [photos, setPhotos] = useState<UploadedPhoto[]>([]);
const [imageUrl, setImageUrl] = useState<string>("");

Uploading Files

Image files are validated and uploaded to Supabase Storage. Public URLs are used to render images in the UI.

ts
const allowedTypes = ["image/jpeg", "image/png", "image/webp"];
const maxSize = 5 * 1024 * 1024;

const uploadFiles = async (files: FileList | File[]) => {
  const validFiles = Array.from(files).filter(
    (file) => allowedTypes.includes(file.type) && file.size <= maxSize
  );
  if (validFiles.length === 0) return;

  const uploadedPhotos: UploadedPhoto[] = [];
  for (const file of validFiles) {
    const fileName = `${uuidv4()}_${file.name}`;
    const { error: uploadError } = await supabase.storage
      .from("locations")
      .upload(fileName, file);
    if (uploadError) throw new Error(uploadError.message);
    const { data } = supabase.storage.from("locations").getPublicUrl(fileName);
    uploadedPhotos.push({ url: data.publicUrl, caption: "" });
  }

  setPhotos((prev) => [...prev, ...uploadedPhotos]);
};

Adding External URLs

External images can be added by entering a valid URL.

ts
const handleAddUrl = () => {
  try {
    new URL(imageUrl);
    setPhotos((prev) => [...prev, { url: imageUrl, caption: "" }]);
    setImageUrl("");
  } catch {
    setError("Invalid URL format.");
  }
};

Drag-and-Drop Ordering

@dnd-kit is used to enable drag-and-drop reordering of image cards.

tsx
<DndContext sensors={sensors} onDragEnd={handleDragEnd}>
  <SortableContext
    items={photos.map((_, i) => `photo-${i}`)}
    strategy={verticalListSortingStrategy}
  >
    <div className="grid grid-cols-2 gap-4">
      {photos.map((photo, index) => (
        <SortablePhoto
          key={`photo-${index}`}
          id={`photo-${index}`}
          index={index}
          photo={photo}
          onRemove={() => removePhoto(index)}
          onMoveUp={() => movePhoto(index, index - 1)}
          onMoveDown={() => movePhoto(index, index + 1)}
          onCaptionChange={(caption) => handleCaptionChange(index, caption)}
          isFirst={index === 0}
          isLast={index === photos.length - 1}
        />
      ))}
    </div>
  </SortableContext>
</DndContext>

Editable Captions & Controls

Each image has an input for a caption and buttons to move or delete the item. The order updates immediately in state.

Full Code

Click here to view the full code.

Conclusion

This component handles common image uploading needs in React projects. It integrates file validation, reordering, and cloud storage access with a straightforward interface. Suitable for use cases like image galleries, product listings, or content upload forms.