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