How to Upload Files from a React Form

Upload files from a React form with a file input, FormData, and fetch. Read the File, show a preview, and handle the request states.

7 min read

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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
{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.

index.jsindex.js
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.

App.jsxApp.jsx
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.

App.jsxApp.jsx
<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

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.
RunePowered by Rune AI

Frequently Asked Questions

How do I read the selected file in React?

Read the input's files property in the change handler. It is a FileList, so use e.target.files[0] for a single file and loop over it for multiple files.

Do I need multipart form data to upload a file?

Yes. Build a FormData object, append the File under a field name, and send it with fetch or XMLHttpRequest. The browser sets the multipart boundary automatically.

Does the accept attribute prevent wrong file types?

No. accept only hints at allowed types in the picker. Validate the file's type and size in your own code, and validate again on the server.

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.