I've the following codes:
http-common.ts
import axios from 'axios';
export default axios.create({
baseURL: window.location.origin,
headers: {
'Content-type': 'application/json',
},
});
types.ts
enum UploadStatus {
NONE,
UPLOADING,
DONE,
}
export default UploadStatus;
UploadForm.tsx
import * as React from 'react';
import http from './http-common';
import UploadStatus from './types';
type UploadItem = {
file: File,
progress: number,
uploadSuccess: boolean | undefined,
error: string,
};
const sendUpload = (file: File, onUploadProgress: (progressEvent: any) => void) => {
const formData = new FormData();
formData.append('file', file);
return http.post('/api/uploadFile', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
onUploadProgress,
});
};
const UploadForm = () => {
const [fileSelection, setSelection] = useState<UploadItem[]>([]);
const [currUploadStatus, setUploadStatus] = useState<UploadStatus>(UploadStatus.NONE);
useEffect(() => {
if (currUploadStatus === UploadStatus.UPLOADING) {
const promises: Promise<void>[] = [];
// Problem code to be discussed
Promise.allSettled(promises)
.then(() => setUploadStatus(UploadStatus.DONE));
}
}, [currUploadStatus]);
const handleFileSelectChange = (event: React.ChangeEvent<HTMLInputElement>) => {
if (event.target.files !== null) {
let newSelection: UploadItem[] = Array.from(event.target.files).map((currFile) => ({
file: currFile,
progress: 0,
uploadSuccess: undefined,
error: '',
isChecked: false,
}));
if (currUploadStatus === UploadStatus.NONE) {
newSelection = newSelection.reduce((currSelection, newSelItem) => {
if (
currSelection.every((currSelItem) => currSelItem.file.name !== newSelItem.file.name)
) {
currSelection.push(newSelItem);
}
return currSelection;
}, [...fileSelection]);
}
setSelection(newSelection);
}
};
// Rest of code
}
I'm trying to upload files from the client to the server. The second parameter in sendUpload
, onUploadProgress
, is a function returning the current file upload progress to let me render a progress bar on-screen.
In my first attempt, useEffect
is implemented as follows:
useEffect(() => {
if (currUploadStatus === UploadStatus.UPLOADING) {
const promises: Promise<void>[] = [];
fileSelection.forEach((currUploadItem) => {
const promise: Promise<void> = sendUpload(
currUploadItem.file,
(event: any) => {
setSelection(() => fileSelection.map((currMapItem) => (
currMapItem.file.name === currUploadItem.file.name
? {
...currMapItem,
progress: Math.round((100 * event.loaded) / event.total),
}
: currMapItem
)));
},
)
.then(({ data }) => {
setSelection(() => fileSelection.map((currMapItem) => (
currMapItem.file.name === currUploadItem.file.name
? {
...currMapItem,
uploadSuccess: data.uploadSuccess,
}
: currMapItem
)));
})
.catch((error) => {
setSelection(() => fileSelection.map((currMapItem) => (
currMapItem.file.name === currUploadItem.file.name
? {
...currMapItem,
error,
uploadSuccess: false,
}
: currMapItem
)));
});
promises.push(promise);
});
Promise.allSettled(promises)
.then(() => setUploadStatus(UploadStatus.DONE));
}
}, [currUploadStatus]);
In the above useEffect
code, I observed that when the file is being uploaded (i.e. the sendUpload(currUploadItem.file, ...)
portion), the progress is being properly updated into the fileSelection
state. When I throttle my browser, I can see my progress bars correctly filling up over time.
However, once the promise resolves and I get to the .then(...)
portion of the code, the progress value goes back to 0. In the then(...)
code segment, uploadSuccess
is correctly updated to true, and this value is successfully saved to state and stays that way. In other words, I can see upload success messages on the screen, but my progress bar has reset to 0% after reaching 100%.
In my second attempt, I changed the code to as follows:
useEffect(() => {
if (currUploadStatus === UploadStatus.UPLOADING) {
const promises: Promise<void>[] = [];
for (const [idx, item] of fileSelection.entries()) {
const promise: Promise<void> = sendUpload(
item.file,
(event: any) => {
const updatedSelection = [...fileSelection];
updatedSelection[idx].progress = Math.round((100 * event.loaded) / event.total);
setSelection(updatedSelection);
},
)
.then(({ data }) => {
const updatedSelection = [...fileSelection];
updatedSelection[idx].uploadSuccess = data.uploadSuccess;
setSelection(updatedSelection);
})
.catch((error) => {
const updatedSelection = [...fileSelection];
updatedSelection[idx] = {
...updatedSelection[idx],
error,
uploadSuccess: false,
};
setSelection(updatedSelection);
});
promises.push(promise);
}
Promise.allSettled(promises)
.then(() => setUploadStatus(UploadStatus.DONE));
}
}, [currUploadStatus]);
In this second version, the code works perfectly. sendUpload(currUploadItem.file, ...)
updates my progress bars correctly. When it reaches 100%, the upload succeeds and the promise resolves, and the rendered progress bars stays at 100%. then(...)
completes, and uploadSuccess
is updated to true. My success messages appear on screen, and the progress bars stay full, which is the correct behaviour.
Why did the first version of code fail but the second succeed? It seems to me that both versions are doing the exact same thing:
- Iterate through each item in
fileSelection
. - For each item, upload the file to server via an
axios
promise, and get the upload progress in realtime - When
axios
updates the upload progress, create a new array. Insert the items from the old array into this new one. For the array item whose file is being sent, update its progress value in realtime. Set state. - When the upload is done,
progress
is at 100. The promise now resolves and executes.then(...)
. - Create a new array. Insert the items from the old array into this new one. For the array item whose file is being sent,
progress
should still be at 100. UpdateuploadSuccess
to the boolean value sent from the server. Set state.
I had thought both versions of code are doing the same things described above. Yet for some reason, the first version saved progress
100 to state at step 4 but it went back to 0 at step 5. The second version saved progress
100 to state at step 4, but at step 5 it remained at 100 as it should.
What happened?