Controlled file input components in React
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 theonChange
callback, because those are immutable and tied to the DOMinput
. It passes an array with the selected files, created from theFileList
. - 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 viaprops
. 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.