React TypeScript: How to set multiple files to state?

Viewed 5117

I'm facing a problem, probably a very simple one, but I'm stuck in since hours so I would like to ask for your help.

I have an simple File Input and I want to set this files to state to upload the files later when submitting the form.

const [inputTattoos, setInputTattoos] = useState<[{}]>();

const handleImageChange = async ({
    currentTarget: input,
  }: React.ChangeEvent<HTMLInputElement>) => {
    if (input.files === null) return;

    console.log(input.files);

    setInputTattoos([{ ...input.files }]);

With this code I'm able to write the files into state, but this is not the way I want to store it on State because my State looks like this:

enter image description here

I have an Array and inside it an object with objects. What I'm actually get from input.files is just an array with objects but I'm not able to store this input.files like I get it on my console. I tried a lot of solutions but this is the only way I found which works. With other solutions I always get an empty object or a FileList(undefined) in State for example with this solution:

const [inputTattoos, setInputTattoos] = useState<FileList>()
const handleImageChange = async ({
    currentTarget: input,
  }: React.ChangeEvent<HTMLInputElement>) => {
    if (input.files === null) return;

    console.log(input.files);

setInputTattoos(input.files);

What's wrong here? Thank you!

1 Answers

I would just store the file objects, no object wrappers or anything:

const [inputTattoos, setInputTattoos] = useState<File[]>([]);
// −−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−−^^^^^^−−^^
const handleImageChange = ({
  currentTarget: {files},
}: React.ChangeEvent<HTMLInputElement>) => {
    if (files && files.length) {
        setInputTattoos(existing => [...existing, ...files]);
// −−−−−−−−−−−−−−−−−−−−−^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
    }
    // ...
}

A couple of notes on that:

  1. I've removed async. Change handlers aren't async functions, and nothing is going to use the promise an async function would return.

  2. I've destructured the files property into its own parameter, so that TypeScript knows it can't change between the guard and the state setter callback. (That's also important because you're not supposed to access properties from React's synthetic event objects asynchronously unless you call persist.)

  3. I've used the callback version of the state setter. That's important when setting a state item based on its existing value (in this case, the previous contents).

  4. The above relies on files (from the input) being iterable, which it is in modern browsers but not in some slightly-older browsers.

Re #4, if you need to work around that for slightly-older browsers:

const [inputTattoos, setInputTattoos] = useState<File[]>([]);
const handleImageChange = ({
  currentTarget: {files},
}: React.ChangeEvent<HTMLInputElement>) => {
    if (files && files.length) {
        setInputTattoos(existing => existing.concat(Array.from(files))); // *** Only change is here
    }
    // ...
}

The change there is the callback:

existing => existing.concat(Array.from(files))

Note that since files is a FileList, not an array, we need to convert it to an array for concat to handle it properly.

Array.from is just a couple of years old but easily polyfilled; if you don't want to do that, here's an alternative using nothing modern (other than the arrow function):

existing => existing.concat(Array.prototype.slice.call(files))

Here's a complete example using Array.from for that part:

const { useState } = React;

function Example() {
    const [inputTattoos, setInputTattoos] = useState/*<File[]>*/([]);
    const [inputKey, setInputKey] = useState(0);
    const handleImageChange = ({
      currentTarget: {file},
    }/*: React.ChangeEvent<HTMLInputElement>*/) => {
        if (files && files.length) {
            setInputTattoos(existing => existing.concat(Array.from(files)));
        }
        // Reset the input by forcing a new one
        setInputKey(key => key + 1);
    }

    return (
        <form>
            Selected tattoos ({inputTattoos.length}):
            <ul>
                {inputTattoos.map((file, index) =>
                    <li key={index}>{file.name}</li>
                )}
            </ul>
            Add: <input key={inputKey} type="file" onChange={handleImageChange} />
        </form>
    );
}

ReactDOM.render(<Example/>, document.getElementById("root"));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.13.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.13.0/umd/react-dom.production.min.js"></script>

Here's a running TypeScript version.

Related