英文:
How can I use a fresh useState value in a function called from a looped function including a timeout in React?
问题
In this code, I would like the output to print "Output:,1,2,3,4,5". But the output variable is stale because I iteratively call the function after a promise is returned from a closure.
What is an efficient way to keep a loop with this timed sleep effect and get my desired result?
英文:
In this code I would like the output to print "Output:,1,2,3,4,5"
But the output variable is stale because I iteratively call the function after a promise is returned from a closure.
What is an efficient way to keep a loop with this timed sleep effect, and get my desired result?
Code Sandbox: https://codesandbox.io/s/hidden-shape-6uh5jm?file=/src/App.js:0-1092
import "./styles.css";
import React, { useEffect, useState } from "react";
export default function App() {
const [output, setOutput] = useState("Output: ");
const [isLooping, setIsLooping] = useState(false);
const sleep = (ms) => {
return function (x) {
return new Promise((resolve) => setTimeout(() => resolve(x), ms));
};
};
const startLoop = () => {
if (isLooping) {
console.log("Click ignored - because is Looping");
return;
} else {
setIsLooping(true);
runLoop();
}
};
const runLoop = (maxIterations = 0) => {
setOutput(output + ", " + maxIterations);
if (maxIterations >= 5) {
setIsLooping(false);
return;
}
sleep(1000)().then(() => {
runLoop(maxIterations + 1);
});
};
useEffect(() => {
startLoop();
}, []);
return (
<div className="App">
<p>{output}</p>
<button
value="Start Loop"
onClick={() => {
startLoop();
}}
>
Start loop
</button>
{isLooping && <p>Is Looping</p>}
</div>
);
}
答案1
得分: 0
以下是翻译好的部分:
任何时候 下一个 状态值依赖于 前一个 状态值,例如,像将值连接到前一个值的字符串连接,您将希望使用函数式状态更新。
更新 runLoop
处理程序以使用函数式状态更新,这实际上是一个相当琐碎的更改:从 setOutput(output + ", " + maxIterations);
到 setOutput(output => output + ", " + maxIterations);
。
const runLoop = (maxIterations = 0) => {
setOutput(output => output + ", " + maxIterations);
if (maxIterations >= 5) {
setIsLooping(false);
return;
}
sleep(1000)().then(() => {
runLoop(maxIterations + 1);
});
};
但是,这会产生奇怪的输出:"Output: , 0, 1, 2, 3, 4, 5"
。
这基本上是一个经典的栅栏问题。
这是将要呈现的 UI 与要从中呈现的数据混合到状态中的结果。请记住,在React中,UI(例如,呈现的内容)是状态和属性的函数。React状态应该仅包含您需要存储的数据,而React组件将状态映射到呈现的UI。
这是一个示例,仅存储 maxIterations
值并从该状态呈现计算的输出字符串。
import "./styles.css";
import React, { useEffect, useState } from "react";
export default function App() {
const [output, setOutput] = useState([]); // <-- 初始空数组
const [isLooping, setIsLooping] = useState(false);
const sleep = (ms) => {
return function (x) {
return new Promise((resolve) => setTimeout(() => resolve(x), ms));
};
};
const startLoop = () => {
if (isLooping) {
console.log("点击被忽略 - 因为正在循环");
return;
} else {
// 为下一次循环重置数组
setOutput([]);
setIsLooping(true);
runLoop();
}
};
const runLoop = (maxIterations = 0) => {
setOutput((output) => output.concat(maxIterations)); // <-- 追加新值,返回新数组
if (maxIterations >= 5) {
setIsLooping(false);
return;
}
sleep(1000)().then(() => {
runLoop(maxIterations + 1);
});
};
useEffect(() => {
startLoop();
}, []);
return (
<div className="App">
<p>Output: {output.join(", ")}</p> // <-- 计算呈现的UI
<button
value="Start Loop"
onClick={() => {
startLoop();
}}
>
Start loop
</button>
{isLooping && <p>Is Looping</p>}
</div>
);
}
现在呈现的输出将更符合预期:"Output: 0, 1, 2, 3, 4, 5"
。
演示
英文:
Any time the next state value depends on the previous state value, e.g. like string concatenation of a value to the previous value, you will want to use a functional state update.
Update the runLoop
handler to use a functional state update, it's really a rather trivial change: setOutput(output + ", " + maxIterations);
to setOutput(output => output + ", " + maxIterations);
.
const runLoop = (maxIterations = 0) => {
setOutput(output => output + ", " + maxIterations);
if (maxIterations >= 5) {
setIsLooping(false);
return;
}
sleep(1000)().then(() => {
runLoop(maxIterations + 1);
});
};
This results in an odd output though: "Output: , 0, 1, 2, 3, 4, 5"
This is basically a classic fencepost problem.
This is the result of mixing the UI you want to render with the data you want to render it from, all into state. Keep in mind that in React, UI (e.g. what is rendered) is a function of state and props. The React state should contain just the data you need to store and the React component maps the state to the rendered UI.
Here's an example of storing just the maxIterations
value and rendering the computed output string from that state.
import "./styles.css";
import React, { useEffect, useState } from "react";
export default function App() {
const [output, setOutput] = useState([]); // <-- initial empty array
const [isLooping, setIsLooping] = useState(false);
const sleep = (ms) => {
return function (x) {
return new Promise((resolve) => setTimeout(() => resolve(x), ms));
};
};
const startLoop = () => {
if (isLooping) {
console.log("Click ignored - because is Looping");
return;
} else {
// reset array for next looping
setOutput([]);
setIsLooping(true);
runLoop();
}
};
const runLoop = (maxIterations = 0) => {
setOutput((output) => output.concat(maxIterations)); // <-- append new value, return new array
if (maxIterations >= 5) {
setIsLooping(false);
return;
}
sleep(1000)().then(() => {
runLoop(maxIterations + 1);
});
};
useEffect(() => {
startLoop();
}, []);
return (
<div className="App">
<p>Output: {output.join(", ")}</p> // <-- compute rendered UI
<button
value="Start Loop"
onClick={() => {
startLoop();
}}
>
Start loop
</button>
{isLooping && <p>Is Looping</p>}
</div>
);
}
The rendered output will now be the more expected "Output: 0, 1, 2, 3, 4, 5"
Demo
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论