当承诺解决时,状态更改意外重置
State changes unexpectedly resets when promise resolves
我有以下代码:
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
}
我正在尝试将文件从客户端上传到服务器。 sendUpload
中的第二个参数,onUploadProgress
,是一个返回当前文件上传进度的函数,让我在屏幕上渲染一个进度条。
在我的第一次尝试中,useEffect
实现如下:
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]);
在上面的 useEffect
代码中,我观察到当文件正在上传时(即 sendUpload(currUploadItem.file, ...)
部分),进度正在正确更新为 fileSelection
状态。当我限制我的浏览器时,我可以看到我的进度条随着时间的推移正确填满。
但是,一旦 promise 解决并且我到达代码的 .then(...)
部分,进度值将返回 0。在 then(...)
代码段中,uploadSuccess
是正确更新为 true,并且此值已成功保存到状态并保持该状态。换句话说,我可以在屏幕上看到上传成功的消息,但是我的进度条在达到100%后重置为0%。
在第二次尝试中,我将代码更改为如下:
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]);
在第二个版本中,代码运行完美。 sendUpload(currUploadItem.file, ...)
正确更新我的进度条。当达到100%时,上传成功,promise resolve,渲染进度条停留在100%。 then(...)
完成,uploadSuccess
更新为 true。我的成功消息出现在屏幕上,进度条保持完整,这是正确的行为。
为什么第一个版本的代码失败了,而第二个版本成功了?在我看来,两个版本都在做完全相同的事情:
- 遍历
fileSelection
中的每个项目。
- 对于每个项目,通过
axios
承诺将文件上传到服务器,并实时获取上传进度
- 当
axios
更新上传进度时,新建一个数组。将旧数组中的项目插入到这个新数组中。对于正在发送文件的数组项,实时更新其进度值。设置状态。
- 上传完成后,
progress
为 100。承诺现在解析并执行 .then(...)
。
- 创建一个新数组。将旧数组中的项目插入到这个新数组中。对于正在发送文件的数组项,
progress
仍应为 100。将 uploadSuccess
更新为从服务器发送的布尔值。设置状态。
我原以为两个版本的代码都在做上述相同的事情。然而由于某种原因,第一个版本在第 4 步将 progress
100 保存到状态,但在第 5 步返回到 0。第二个版本将 progress
100 保存到第 4 步的状态,但在第 5 步它应该保持在 100。
发生了什么事?
您的第二个代码复制了数组,但它仍然包含相同的对象,然后其中一个对象在行突变
updatedSelection[idx].progress = Math.round((100 * event.loaded) / event.total);
您的第一个代码也通过使用扩展语法正确地克隆了对象。但是,问题在于它在每个事件中一次又一次地以效果执行时的旧 fileSelection
值作为起点。请注意,如果您一次上传多个文件,情况会更糟。
原因是关闭了过时的 const
ant,如 , , How to deal with stale state values inside of a useEffect closure? or this blog article 中所述。
要解决此问题,请使用 setSelection
的回调版本:
useEffect(() => {
if (currUploadStatus === UploadStatus.UPLOADING) {
const promises = fileSelection.map(uploadItem =>
sendUpload(
uploadItem.file,
(event: any) => {
setSelection(currSelection =>
// ^^^^^^^^^^^^^
currSelection.map(item => item.file.name === uploadItem.file.name
? {
...item,
progress: Math.round((100 * event.loaded) / event.total),
}
: item
)
);
},
)
.then(({data}) => ({
uploadSuccess: data.uploadSuccess
}, error => ({
error,
uploadSuccess: false
})
.then(result => {
setSelection(currSelection =>
// ^^^^^^^^^^^^^
currSelection.map(item => item.file.name === uploadItem.file.name
? {
...item,
...result,
}
: item
)
);
})
);
Promise.all(promises)
.then(() => setUploadStatus(UploadStatus.DONE));
}
}, [currUploadStatus]);
我有以下代码:
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
}
我正在尝试将文件从客户端上传到服务器。 sendUpload
中的第二个参数,onUploadProgress
,是一个返回当前文件上传进度的函数,让我在屏幕上渲染一个进度条。
在我的第一次尝试中,useEffect
实现如下:
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]);
在上面的 useEffect
代码中,我观察到当文件正在上传时(即 sendUpload(currUploadItem.file, ...)
部分),进度正在正确更新为 fileSelection
状态。当我限制我的浏览器时,我可以看到我的进度条随着时间的推移正确填满。
但是,一旦 promise 解决并且我到达代码的 .then(...)
部分,进度值将返回 0。在 then(...)
代码段中,uploadSuccess
是正确更新为 true,并且此值已成功保存到状态并保持该状态。换句话说,我可以在屏幕上看到上传成功的消息,但是我的进度条在达到100%后重置为0%。
在第二次尝试中,我将代码更改为如下:
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]);
在第二个版本中,代码运行完美。 sendUpload(currUploadItem.file, ...)
正确更新我的进度条。当达到100%时,上传成功,promise resolve,渲染进度条停留在100%。 then(...)
完成,uploadSuccess
更新为 true。我的成功消息出现在屏幕上,进度条保持完整,这是正确的行为。
为什么第一个版本的代码失败了,而第二个版本成功了?在我看来,两个版本都在做完全相同的事情:
- 遍历
fileSelection
中的每个项目。 - 对于每个项目,通过
axios
承诺将文件上传到服务器,并实时获取上传进度 - 当
axios
更新上传进度时,新建一个数组。将旧数组中的项目插入到这个新数组中。对于正在发送文件的数组项,实时更新其进度值。设置状态。 - 上传完成后,
progress
为 100。承诺现在解析并执行.then(...)
。 - 创建一个新数组。将旧数组中的项目插入到这个新数组中。对于正在发送文件的数组项,
progress
仍应为 100。将uploadSuccess
更新为从服务器发送的布尔值。设置状态。
我原以为两个版本的代码都在做上述相同的事情。然而由于某种原因,第一个版本在第 4 步将 progress
100 保存到状态,但在第 5 步返回到 0。第二个版本将 progress
100 保存到第 4 步的状态,但在第 5 步它应该保持在 100。
发生了什么事?
您的第二个代码复制了数组,但它仍然包含相同的对象,然后其中一个对象在行突变
updatedSelection[idx].progress = Math.round((100 * event.loaded) / event.total);
您的第一个代码也通过使用扩展语法正确地克隆了对象。但是,问题在于它在每个事件中一次又一次地以效果执行时的旧 fileSelection
值作为起点。请注意,如果您一次上传多个文件,情况会更糟。
原因是关闭了过时的 const
ant,如
要解决此问题,请使用 setSelection
的回调版本:
useEffect(() => {
if (currUploadStatus === UploadStatus.UPLOADING) {
const promises = fileSelection.map(uploadItem =>
sendUpload(
uploadItem.file,
(event: any) => {
setSelection(currSelection =>
// ^^^^^^^^^^^^^
currSelection.map(item => item.file.name === uploadItem.file.name
? {
...item,
progress: Math.round((100 * event.loaded) / event.total),
}
: item
)
);
},
)
.then(({data}) => ({
uploadSuccess: data.uploadSuccess
}, error => ({
error,
uploadSuccess: false
})
.then(result => {
setSelection(currSelection =>
// ^^^^^^^^^^^^^
currSelection.map(item => item.file.name === uploadItem.file.name
? {
...item,
...result,
}
: item
)
);
})
);
Promise.all(promises)
.then(() => setUploadStatus(UploadStatus.DONE));
}
}, [currUploadStatus]);