Controlled file input components in React

Asís García
Trabe
Published in
2 min readAug 5, 2019
Photo by Vincent Botta on Unsplash

The problem

React documentation states that:

In React, an <input type="file" /> is always an uncontrolled component because its value can only be set by a user, and not programmatically.

That makes sense, because an HTML <input type="file" /> won’t let you set its value (see note 2 here).

But a file input element has another attribute, files, that you can use to get a list of the selected files. And you can set the files attribute via the DOM. You can create your own component and use useEffect and useRef to simulate a controlled file input component:

The problem is, the component above isn’t controlled. If you render it like this:

<FileInput value="" />

You’d expect it to always be empty. But if the user clicks the file input, whatever she selects will be used as the input value, no matter what your prop says. And there’s nothing you can do about it. Except…

The solution

Stop using the native file input. 😅

I mean, you have to use a native file input so the user can select files (well, she can also drop some files into the browser, but that’s another story), but you can wrap it inside another component, which you can then use as a controlled one.

Your component will use a hidden file input to let the user select files, and it will expose a classic controlled component API:

  • A value prop to set the list of selected files.
  • An onChange callback that will be called with the new list of files whenever that list changes.

Some things to note:

  • The component does not emit a change event like its native counterpart. It just calls onChange with the list of selected files.
  • In fact, the component does not pass a FileList to the onChange callback, because those are immutable and tied to the DOM input. It passes an array with the selected files, created from the FileList.
  • If you escape the React world and start to do some imperative black magic, you’ll notice that the DOM input might have a different value than the one the wrapper component is receiving via props. Well, that’s “intended behavior”™️. We’ve already established that you can’t control the native input.
  • While it’s not shown in the implementation above, you could add more UI elements to let the user remove specific files, or append new files to the ones previously selected.

I know, this component feels like cheating. Because we are cheating, big time 😅. But trying to bend the way the browser works leads to all kind of pain. Keep it simple and embrace the constraints of the platform: you’ll be happier.

--

--