2

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!

3
  • 1
    "...but this is not the way I want to store it on State..." How do you want to store it? Commented Aug 20, 2020 at 8:26
  • I want to store it like I get it on my console.log(input.files). I have an Array "FileList" with objects for each File [File objects]. But what I'm storing right now to state is an Array with an object inside it the objects for each File [{File objects}]. Commented Aug 20, 2020 at 8:31
  • Okay, good, that's what I assumed when writing my answer. :-) Commented Aug 20, 2020 at 8:33

1 Answer 1

9

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.

Sign up to request clarification or add additional context in comments.

8 Comments

Thank you! But how can I pass the current state to the callback? I get "it's undefined"
@wyndham007 - You don't pass the current state to the callback, React does. That's how React state setters work, you can either pass them the data directly, or you can pass them a function and they'll call it with the then-current information. If you've updated your useState to be as I've shown above, the current state won't be undefined. (It's undefined because your original code doesn't pass anything into useState.) I've fixed the concat thing I was talking about above as well.
@wyndham007 - I've updated the answer with a complete example so you can see it in action (with the TypeScript-specific parts commented out).
Thank you very much for your example!! Now typescript compiler says: Argument of type 'FileList | null' is not assignable to parameter of type 'ArrayLike<File>'. Type 'null' is not assignable to type 'ArrayLike<File>'
@wyndham007 - The file objects are stored in state, they aren't just {}, as you can tell from the live example showing you the names of the files. As for what a Blob is, MDN is your friend: File objects are subclasses of Blob objects. Note that if you're going to store these in state and then submit them, you'll need to use FormData when building your form submission, since it can read files.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.