React Hooks: what is the correct way to handle async setState inside component ? (uploadProgress axios)

Viewed 728

We want to post multiple files in parallel and display upload progress.

Our first attempt to solve it was to use setState and the previous state in uploadProgress callback:

const FileUpload = () => {
  const [pendingUploads, setPendingUploads] = useState({});

  // For clarity: files = Record<string, {id: string, progress: string, data: FormData}>
  const handleChange = files => {
    setPendingUploads(files);

    const handleUploadProgress = (progressEvent, id) => {
      const percentCompleted = (progressEvent.loaded * 100) / progressEvent.total;

      // Problem: pendingUploads is always empty, we do not get the updated value from last update 
      setPendingUploads({
        ...pendingUploads,
        [id]: {
          ...pendingUploads[id],
          progress: `${percentCompleted}%`
        }
      });
    };

    files.forEach(file => {
      axios.post("/files", file.data, {
        onUploadProgress: e => handleUploadProgress(e, file.id)
      });
    });
  };

  return (
    <>
      <div>
        {_.map(pendingUpload, (pendingUpload, idx) => (
          <div key={idx}>
            {pendingUpload.id}: {pendingUpload.progress}
          </div>
        ))}
      </div>

      <input multiple type="file" onChange={handleChange} />
    </>
  );
};

pendingUploads is always empty so the progress value is display for only 1 file at a time.

What would be the correct way to do it ? We have 2 working example so far:

1) Solution 1

  • Keep a map of pending upload in state (pendingUploads)
  • Trigger a progress update (progressUpdate) using setState inside handleUploadProgress (axios callback)
  • Update pendingUploads in useEffect when progressUpdate change
const FileUpload = () => {
  const [pendingUploads, setPendingUploads] = useState({});
  const [progressUpdate, setProgressUpdate] = useState({ id: "", value: "" });

  useEffect(() => {
    setPendingUploads({
      ...pendingUploads,
      [progressUpdate.id]: {
        ...pendingUploads[progressUpdate.id],
        progress: progressUpdate.value
      }
    });
  }, [progressUpdate]);

  const handleUploadProgress = (progressEvent, id) => {
    const percentCompleted = (progressEvent.loaded * 100) / progressEvent.total;
    setProgressUpdate({ id, value: `${percentCompleted}%` });
  };

  // files = Record<string, {id: string, progress: string, data: FormData}>
  const handleChange = files => {
    setPendingUploads(files);
    files.forEach(file => {
      axios.post("/files", file.data, {
        onUploadProgress: e => handleUploadProgress(e, file.id)
      });
    });
  };

  return (
    <div>
      <div>
        {_.map(pendingUploads, (pendingUpload, idx) => (
          <div key={idx}>
            {pendingUpload.id}: {pendingUpload.progress}
          </div>
        ))}
      </div>

      <input multiple type="file" onChange={handleChange} />
    </div>
  );
};



2) Solution 2

  • Mutate the files in uploadProgress callback and use the mutated value to update the state of component
const FileUpload = () => {

  const [pendingUploads, setPendingUploads] = useState({});

  // files = Record<string, {id: string, progress: string, data: FormData}>
  const handleChange = files => {
    const handleUploadProgress = (progressEvent, id) => {
      const percentCompleted = (progressEvent.loaded * 100) / progressEvent.total;
      files[id].progress = `${percentCompleted}%`;
      setPendingUploads({ ...files });
    };

    files.forEach(file => {
      axios.post("/files", file.data, {
        onUploadProgress: e => handleUploadProgress(e, file.id)
      });
    });
  };

  return (
    <>
      <div>
        {_.map(pendingUpload, (pendingUpload, idx) => (
          <div key={idx}>
            {pendingUpload.id}: {pendingUpload.progress}
          </div>
        ))}
      </div>

      <input multiple type="file" onChange={handleChange} />
    </>
  );
};

Any feedback or working example showing best practice for this particular case are welcome.

Thanks

1 Answers

You can't use pendingUploads const inside response callbacks, cause it will reference an outdated version. Instead, consider using previous state setState signature like that: setPendingUploads((prevPendingUploads) => {return {...prevPendingUploads, /* updates here */}}

Related