The post cover image upload has a structural challenge: the post editor needs a cover image URL to include in the post payload, but the URL only exists after the image is uploaded. There are two approaches: upload first (upload the image immediately on selection, get back a URL, then include that URL when creating the post) or upload with the post (send both the image and the metadata in one multipart request). The upload-first approach is simpler on the React side and more flexible — it works with both POST and PATCH without changing the endpoint signature.
Upload-First Approach
// Add to apiSlice.js
uploadPostImage: builder.mutation({
query: (formData) => ({
url: "/posts/upload-image",
method: "POST",
body: formData,
formData: true,
}),
}),
// src/pages/PostEditorPage.jsx (cover image section)
import { useState, useCallback } from "react";
import { useUploadPostImageMutation } from "@/store/apiSlice";
import ImagePicker from "@/components/ui/ImagePicker";
function CoverImageSection({ coverImageUrl, onChange }) {
const [uploadImage, { isLoading: isUploading }] = useUploadPostImageMutation();
// When user selects a file, upload immediately and give parent the URL
const handleFileSelect = useCallback(async (file) => {
if (!file) {
onChange(null); // user cleared the selection
return;
}
const formData = new FormData();
formData.append("image", file);
try {
const { url } = await uploadImage(formData).unwrap();
onChange(url); // pass the server URL to the parent form
} catch (err) {
// Upload failed — clear the selection in the picker
onChange(null);
}
}, [uploadImage, onChange]);
return (
<div className="space-y-2">
<label className="block text-sm font-medium">Cover Image</label>
{/* Show existing cover when editing */}
{coverImageUrl && !coverImageUrl.startsWith("blob:") && (
<div className="relative">
<img src={coverImageUrl} alt="Cover"
className="w-full h-40 object-cover rounded-lg" />
<button
type="button"
onClick={() => onChange(null)}
className="absolute top-2 right-2 bg-black/50 text-white
px-2 py-1 text-xs rounded"
>
Remove
</button>
</div>
)}
<ImagePicker
label={coverImageUrl ? "Change cover" : "Add cover image"}
onChange={handleFileSelect}
/>
{isUploading && (
<p className="text-sm text-blue-600 animate-pulse">Uploading image…</p>
)}
</div>
);
}
// In the main PostEditorPage form:
// const [coverImageUrl, setCoverImageUrl] = useState(existingPost?.cover_image_url ?? null);
// ...
// payload = { ...formValues, cover_image_url: coverImageUrl };
coverImageUrl = post.cover_image_url, (2) new image uploaded — coverImageUrl = newUrl, (3) cover image removed — coverImageUrl = null. Only include cover_image_url in the PATCH payload when it has actually changed, to avoid unnecessary server-side processing.onSubmit handler. The upload can take several seconds, during which the submit button would appear frozen even though the form data is ready. Upload-first keeps the form submission fast (just JSON) while the image upload happens asynchronously as soon as the user selects it. If the form is submitted before the image finishes uploading, disable the submit button while isUploading is true.Disabling Submit During Image Upload
function PostEditorPage() {
const [isImageUploading, setIsImageUploading] = useState(false);
const [form, setForm] = useState({ ...INITIAL_FORM, cover_image_url: null });
function handleCoverChange(url) {
setForm((p) => ({ ...p, cover_image_url: url }));
}
return (
<form onSubmit={handleSubmit} noValidate>
{/* ... other fields ... */}
<CoverImageSection
coverImageUrl={form.cover_image_url}
onChange={handleCoverChange}
onUploadingChange={setIsImageUploading}
/>
<button
type="submit"
disabled={isSubmitting || isImageUploading}
>
{isImageUploading ? "Waiting for image…"
: isSubmitting ? "Saving…"
: "Publish Post"}
</button>
</form>
);
}
Common Mistakes
Mistake 1 — Uploading in onSubmit (slow, blocking UX)
❌ Wrong — entire submit is blocked while image uploads:
async function handleSubmit(formValues) {
const { url } = await uploadImage(formData).unwrap(); // user waits here!
await createPost({ ...formValues, cover_image_url: url });
}
✅ Correct — upload on file selection, save the URL in state, submit only metadata.
Mistake 2 — Not disabling submit while image uploads
❌ Wrong — user submits with cover_image_url: null while image is uploading:
✅ Correct — pass disabled={isSubmitting || isImageUploading} to the submit button.