skip to content

Why can a React <input type="file"> not be a controlled component, and how do you read and clear the user's selection?

level: middleimportance: should knowfreq 35%

answer

  1. the user picks, the page cannot
  2. value is not assignable for security
  3. always uncontrolled, read files instead
  4. empty string is the one allowed write
  5. re-picking the same file fires nothing

basics

~20 s

Browsers forbid script from setting which file is selected, so React cannot drive a file input from state — it is always uncontrolled. Read the selection from the node's files list or from FormData, and clear it by assigning an empty string to the node's value.

solid answer

~50 s

A file input's selection is a security boundary: only the user may choose a file, so assigning anything but an empty string to `input.value` is rejected by the browser. That makes the React pattern of "render the value from state" impossible, and file inputs are always uncontrolled — render them with no `value` prop. You read the selection from the DOM node: `e.target.files` in the change handler, or `inputRef.current.files`, both a `FileList` where `files[0]` is the first `File`; a `FormData` built from the form returns the same `File` objects for that field. You can still keep the chosen `File` in state for a preview or an upload queue — state holds a description of the selection, not the input's value. To clear the field, set `inputRef.current.value = ''`, or remount the input with a changing `key`. That empty-string assignment is also the fix for the gotcha that re-picking the same file fires no change event.

code

jsx · 30 lines
jsx
import { useRef, useState } from 'react';

export function AvatarPicker({ onUpload }) {
  const inputRef = useRef(null);
  const [file, setFile] = useState(null);

  function handleChange(e) {
    setFile(e.target.files[0] ?? null);
  }

  function handleClear() {
    setFile(null);
    inputRef.current.value = ''; // the only assignment the browser allows
  }

  return (
    <div>
      <input ref={inputRef} type="file" accept="image/*" onChange={handleChange} />
      {file && (
        <p>
          {file.name} — {Math.round(file.size / 1024)} KB
          <button type="button" onClick={handleClear}>Remove</button>
        </p>
      )}
      <button type="button" disabled={!file} onClick={() => onUpload(file)}>
        Upload
      </button>
    </div>
  );
}

go deeper

for a junior

Know that a file input is always uncontrolled, that you read e.target.files rather than a value string, and that files[0] can be missing when the user cancels.

for a middle

Explain why the browser refuses to let script set the selection, and show both clearing techniques — assigning an empty string through a ref, or remounting the input with a key.

for a senior

Bring the operational details: guarding size and type before upload, object-URL cleanup for previews, and clearing after consumption so re-picking the same file still fires change.

for a principal

Own the upload contract end to end — where validation actually enforces limits, how large files are chunked or sent direct to storage, and why client-side checks are UX rather than a boundary.

## The security rule underneath If a page could set a file input's value, any site could read arbitrary files off your disk by pointing an input at a path and submitting the form. The platform therefore makes the selection user-only: setting `input.value` to a non-empty string is rejected, and the only assignment the browser honours is the empty string, which clears the selection. That rule collides directly with React's controlled pattern, whose whole premise is that React writes the value onto the node every render. There is nothing React can do about it, so a file input is always uncontrolled — render it with no `value` prop and let the DOM own the choice. ## Reading the selection The interesting data is not the value string (a browser-sanitised fake path) but the `files` property, a `FileList` of `File` objects: ```jsx function Upload() { function handleChange(e) { const file = e.target.files[0]; if (!file) return; // the user cancelled the picker console.log(file.name, file.size, file.type); } return <input type="file" onChange={handleChange} />; } ``` With `multiple`, iterate the whole list — `Array.from(e.target.files)` turns it into a real array. Outside a change handler, a ref gives you the same thing: `inputRef.current.files`. And a `FormData` built from the surrounding form returns `File` objects for that field rather than strings, so the same field reads differently from every other one in the form. Always guard for an empty list. Opening the picker and cancelling leaves `files` empty, and on some platforms it fires a change event, so `files[0]` can be `undefined`. ## Uncontrolled input, controlled UI "Uncontrolled" applies to the input element, not to your component. Keeping the selection in state is normal and useful: ```jsx const [file, setFile] = useState(null); <input type="file" onChange={(e) => setFile(e.target.files[0] ?? null)} /> {file && <p>{file.name} ({Math.round(file.size / 1024)} KB)</p>} ``` State here holds a `File` object you use to render a filename, a size, a preview or an upload progress row. It is never fed back into the input's `value`, which is exactly why this does not count as controlling the input. For an image preview, `URL.createObjectURL(file)` gives you a URL to render, and you release it with `URL.revokeObjectURL` when the preview goes away. ## Clearing the field Two approaches, both legitimate: ```jsx // 1. the one assignment the browser allows inputRef.current.value = ''; // 2. throw the element away and mount a fresh one <input type="file" key={resetCount} onChange={handleChange} /> ``` The first is direct and cheap. The second is useful when several fields reset together — bumping a counter used as a `key` discards the old input and mounts a new, empty one. Calling `form.reset()` on the surrounding form clears file inputs too, along with everything else. ## The gotcha worth mentioning A change event fires when the *value changes*. Pick `report.pdf`, upload it, then pick `report.pdf` again and nothing happens — the value did not change, so no event, so your handler never runs and the UI looks frozen. Clearing the input right after you have consumed the file (`e.target.value = ''` at the end of the change handler) makes every subsequent pick a change, including re-picking the same file. Interviewers like this one because it only shows up in real usage. ## What to say in an interview Lead with the reason rather than the rule: the file selection is user-controlled for security, so `value` is not assignable and the controlled pattern cannot apply. Then the practicalities — read `files`, keep the `File` in state if the UI needs it, clear with `value = ''` or a `key` remount — and finish with the re-picking-the-same-file gotcha, which shows you have shipped an upload flow rather than read about one.

  • A user picks the same file twice in a row and your handler runs only the first time. Why?
    Because change fires when the input's value changes, and re-selecting the identical file leaves it unchanged. Clear the input at the end of your change handler — `e.target.value = ''` — once you have taken the `File` out of `e.target.files`. Every subsequent pick is then a genuine change, including the same file again.
  • How do you show an image preview of the selected file?
    Take the `File` from `e.target.files[0]`, keep it in state, and render `URL.createObjectURL(file)` as the img src. Revoke it with `URL.revokeObjectURL` when the preview is replaced or unmounted, otherwise the object URL pins the file's memory for the lifetime of the document. `FileReader` with a data URL also works but is heavier for large images.
  • Does the accept attribute make client-side file validation unnecessary?
    No. `accept` filters the picker's default view as a convenience; users can override it, and the attribute says nothing about the real bytes. Check `file.type` and `file.size` yourself for a fast user-facing message, and treat the server as the actual gate — client-side checks are UX, never security.

saying these in an interview costs you the question

  • Tries to drive a file input with value from React state
  • Reads input.value expecting a real filesystem path
  • Thinks setting state to null clears the chosen file
  • Assumes files[0] always exists after a change event
  • Treats the accept attribute as a security control

context