3

I'm handling file uploads using react-dropzone, which calls the following function with a list of files:

    const [files, setFiles] = useState([])

    const onDrop = useCallback(async acceptedFiles => {
        uploadFiles(acceptedFiles)
    }, [])

I then upload the files using fetch:

    const uploadFiles = async acceptedFiles => {
        acceptedFiles.forEach(async (file) => {
            setFiles(array => [...array, fileState])

            await fetch('/upload', {
                method: 'POST',
                body: data
            }).then(res => {
                // etc
            })
        })
    }

If I replace the setFiles call with just setFiles([...files, fileState]) (not in a function) it breaks, but why is this?

Alexander Craggs
  • 7,874
  • 4
  • 24
  • 46
  • `setFiles(array => [...array, fileState])` apparently takes a function reference. So if you pass an array it won't make any sense. – codemonkey Feb 17 '21 at 23:51
  • `useState` also returns a function that should take a value in. The first documentation example (https://reactjs.org/docs/hooks-state.html) suggests that you can call `setCount(count + 1)`. – Alexander Craggs Feb 17 '21 at 23:54
  • Oh I see, `setFiles` is actually a useState function. My bad, I thought it was some random function related to react-dropzone. When you say "it breaks", what do you mean exactly? – codemonkey Feb 17 '21 at 23:56
  • `files` seems to render as if it contains only the latest item added to the array. When logging `console.log(files)` within the uploadFiles function it's always empty. This happens even if I've called `setState([...files, fileState])` multiple times. When I change the code ot `setState(array => [...array, fileState])` it works as anticipated. – Alexander Craggs Feb 18 '21 at 00:02
  • Both of those calls should in theory work, I will give you that. Any way to reproduce this in a sandbox? – codemonkey Feb 18 '21 at 00:17
  • When you say 'it breaks', do you mean it errors or it just doesn't work as you expect? – James Feb 18 '21 at 00:18
  • I'll see if I can get a reproducible example. By 'it breaks' I mean the array is always empty and it _appears_ as if `setState()` isn't actually changing the variable (although weirdly it does do a re-render and update the UI?!). This results in multiple calls to setState meaning only the most recent file uploaded is visible. – Alexander Craggs Feb 18 '21 at 00:36

1 Answers1

1

I believe I was able to reproduce the issue you're having. https://codesandbox.io/s/elegant-bouman-9lb5k?file=/src/App.js

Here is the relevant code:

  const [files, setFiles] = useState(["file1", "file2"]);

  const uploadFiles = async (acceptedFiles) => {
    acceptedFiles.forEach(async (file) => {
      //setFiles([...files, file]);
      setFiles(array => [...array, file])

      await fetch("https://httpstat.us/200");
    });
  };

  const testFunc = async (acceptedFiles) => {
    uploadFiles(["file3", "file4"]);
  };

If behaves as expected when called like the above, but if you swap the setFiles calls, only file4 will be appended.

The issue appears to be related to React batching setState calls as they are made in close succession. This post provides the best answer I could find.

So it appears that when you call your setFiles with the callback, React does not batch those requests and thus executes them one after the other.

codemonkey
  • 7,325
  • 5
  • 22
  • 36