给定任务列表:
const [tasks, setTasks] = useState([])
我想在用户输入setTasks(...tasks, aNewTask)
时添加一个任务,然后异步更新该任务的结果:
while (true) {
taskStatus = await getTaskStatus()
setTasks(tasks.map(t => t.id == taskStatus.id ? taskStatus : t))
}
这看起来在逻辑上是正确的。但它不起作用;该任务被添加到列表中,然后被删除。tasks
没有更新,所以在我第一次调用setTasks
之前,查询它会生成原始列表。在使用相同模式时,我看到的最好的解决方法是将useState
封装在一个自定义挂钩中,并将promise作为集值函数的一部分来解析,但即使这样,我也需要tasks
是var
,这样我就可以在本地更新它。
有没有一种更干净的方法,仍然使用异步逻辑?
当您具有在异步操作之后设置状态的效果时,您应该在设置该状态之前检查组件是否仍处于安装状态。
下面是一个在设置状态之前进行检查的示例,当组件卸载时将退出无限循环。该效果没有依赖项,因此将仅在第一次渲染后运行。任务不是效果的依赖项,因为我将回调传递给setTasks。
const { useRef, useEffect, useState } = React;
//helper to check if component is mounted so you won't
// try to set state of an unmounted component
// comes from https://github.com/jmlweb/isMounted/blob/master/index.js
const useIsMounted = () => {
const isMounted = useRef(false);
useEffect(() => {
isMounted.current = true;
return () => (isMounted.current = false);
}, []);
return isMounted;
};
//returns current date after waiting for a second
function getTasksStatus() {
return new Promise(r =>
setTimeout(() => r(Date.now(), 1000))
);
}
function App() {
const [tasks, setTasks] = useState(
Math.floor(Date.now() / 1000)
);
//from the helper to check if component is still mounted
const isMounted = useIsMounted();
useEffect(() => {
//babel of Stack overflow is ancient and doesn't know what
// to do with async so I create a recursive instead
function polling() {
getTasksStatus().then(newTasks => {
//only do something if component is still mounted
if (isMounted.current) {
//pass callback to setTasks so effect doesn't depend on tasks
setTasks(currentTasks =>
Math.floor(newTasks / 1000)
);
//call recursively
polling();
}
});
}
//the async version with "infinite" loop looks like this
// async function polling() {
// //exit loop if component is unmounted
// while (isMounted.current) {
// const newTasks = await getTasksStatus();
// isMounted.current && //make sure component is still mounted
// //pass callback to setTasks so effect doesn't depend on tasks
// setTasks(currentTasks =>
// Math.floor(newTasks / 1000)
// );
// }
// }
polling();
}, [isMounted]);
return <div>{tasks}</div>;
}
ReactDOM.render(<App />, document.getElementById('root'));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<div id="root"></div>
在循环中执行setState
的问题是它只会触发一次。为了让它"轮询",你可以用超时功能包装它:
let timeout = null; //outside class
...
while (true) {
timeout = setTimeout(() => {
taskStatus = await getTaskStatus()
setTasks(tasks.map(t => t.id == taskStatus.id ? taskStatus : t))
}, 1000);
}
CCD_ 8是运行所设置的任务/轮询的毫秒数。
确保在卸载时清除超时,如下所示:
useEffect(() => () => clearTimeout(timeout)));
这样它就不会轮询组件是否不再活动