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
bashnpx 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:
tsimport { 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
tsconst [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.
tsconst 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.
tsconst 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.