Upload files in React by reading the selected File and sending it with FormData. Store the File, show a preview or its name, and handle the request states. The accept attribute only hints at allowed types, so real validation happens in your code and on the server.
The upload itself is an async operation, so it needs the same pending, success, and error states as any request.
Read the selected file
A file input reports its selection through the files property, a FileList of File objects. Store the first file in state so the rest of the component can show it and submit it.
import { useState } from "react";
export default function UploadForm() {
const [file, setFile] = useState(null);
function handleChange(e) {
setFile(e.target.files[0]);
}
return (
<form>
<label htmlFor="avatar">Choose an image</label>
<input id="avatar" type="file" accept="image/*" onChange={handleChange} />
{file && <p>Selected {file.name}</p>}
</form>
);
}Choosing a file stores the File object, and the paragraph shows its name. The accept attribute narrows the picker to images, but the user can still bypass it, so it is a hint rather than a check.
For multiple files, add the multiple attribute and read e.target.files as a list, storing the whole FileList or an array of File objects instead of one item. You cannot set a file input's value from script, so the picker stays the only way to choose a file.
Show a preview with an object URL
For images, create a temporary object URL and point an img element at it. The URL is a browser resource, so revoke it in the Effect cleanup to avoid keeping memory tied to a file you no longer display. Add a preview state to the same component, add useEffect to the react import, and fill the state with an Effect.
const [preview, setPreview] = useState("");
useEffect(() => {
if (!file) {
setPreview("");
return;
}
const url = URL.createObjectURL(file);
setPreview(url);
return () => URL.revokeObjectURL(url);
}, [file]);When the user picks a file, the Effect creates a URL and stores it in preview. When the file changes or the component unmounts, the cleanup revokes the old URL.
{preview && <img src={preview} alt="Selected file preview" />}The img element renders only while a preview URL exists, and the cleanup prevents the browser from holding unused object URLs in memory.
Check type and size before uploading
Validate the File before sending it, because accept does not. Return a message or null from a plain function and show the message when it exists.
function validateFile(file) {
if (file.size > 2 * 1024 * 1024) {
return "File must be under 2 MB.";
}
if (!file.type.startsWith("image/")) {
return "Choose an image file.";
}
return null;
}The size is in bytes, so 2 MB is two times 1024 times 1024. Call this before building FormData and block the upload when it returns a message. The same idea is expanded in React form validation without a library.
Run the check on the selected file before the request and show the returned message next to the input. This keeps a large or wrong file from reaching the server, while the server still repeats the check as the authority.
Upload with FormData and fetch
On submit, append the File to a FormData object and send it with fetch. The browser sets the multipart boundary, so you only pass the body. Use multipart form data for files, not JSON, because JSON cannot carry binary content.
const [status, setStatus] = useState("idle");
async function handleSubmit(e) {
e.preventDefault();
if (!file) {
setStatus("error");
return;
}
const data = new FormData();
data.append("avatar", file);
setStatus("sending");
const response = await fetch("/api/upload", { method: "POST", body: data });
setStatus(response.ok ? "done" : "error");
}The handler guards against an empty selection, then sends the file and records the outcome. Render the three states so the user always knows what is happening.
<button type="submit" disabled={status === "sending"}>
{status === "sending" ? "Uploading..." : "Upload"}
</button>
{status === "done" && <p role="status">Upload complete.</p>}
{status === "error" && <p role="alert">Upload failed. Choose a file and try again.</p>}The button disables while the request is in flight, which prevents duplicate uploads.
Track progress and allow retry
A plain fetch does not report upload progress, but the pending and done states you already track are enough for most forms. When the upload fails, keep the File in state so the user can retry without picking it again, and set the status back to idle before the next attempt. For a progress bar, use XMLHttpRequest with its progress event or a library that supports progress out of the box.
For the server response and its error shape, see how to show server validation errors in React forms. The basic controlled form wiring is in how to build forms in React.
Rune AI
Key Insights
- Read the selected File from e.target.files.
- Append the File to FormData before sending.
- Use URL.createObjectURL for a preview and revoke it in cleanup.
- Check type and size yourself; accept is only a hint.
- Show pending, success, and error states.
Frequently Asked Questions
How do I read the selected file in React?
Do I need multipart form data to upload a file?
Does the accept attribute prevent wrong file types?
Conclusion
Store the selected File in state, validate its type and size, then send it inside FormData with fetch. Show pending, success, and error states while the request runs.
More in this topic
How to Build a Dropdown Menu in React
Build a React dropdown menu with the ARIA menu button pattern. Handle open and close, keyboard arrows, and clicks outside the menu.
How to Animate React Components with Motion
Animate React components with the Motion library. Set up motion, add enter, hover, and exit animations, and respect reduced motion.
Headless UI Components Explained: Logic Without Locked Styling
Understand headless UI components and how libraries like Radix give you unstyled, accessible behavior that you style yourself.