React - 设置状态仅在异步操作中有效
React - Set state work only in async operation
我正在使用 React(带有 TypeScript),我发现了非常奇怪的行为。
在方法“handleAnswer”中,我正在更改变量“isReady”并使用 useEffect 挂钩打印值。但是只有当我在那里放置一个计时器时才会发生变化,当我取出计时器时,变量停止变化,并且我不再在控制台中看到输出。我做错了什么?
export const App = () => {
const [isReady, setIsReady] = useState(true);
const [counter, setCounter] = useState(0);
useEffect(() => { console.log(isReady) },[isReady]);
const handleAnswer = async (isLiked: boolean) => {
setIsReady(false);
// WHEN I REMOVE THIS LINE, IT DOESN'T WORK
await new Promise(resolve => setTimeout(resolve, 10));
setCounter(counter + 1)
setIsReady(true);
}
return (
<div className="App" >
{ isReady ? <div className="main-content"></div> :<CircularProgress /> }
<Button variant="contained" onClick={() => handleAnswer(true)} startIcon={<ThumbUpIcon />}>
click
</Button>
</div>
);
}
export default App;
状态更新被排队并在排队它们的代码返回后作为批处理。因此,如果没有承诺,您的 handleAnswer
代码会这样做:
- 排队状态更新,将
isReady
更改为 false
。
- 排队添加到
counter
的状态更新。
- 排队状态更新,将
isReady
更改为 true
。
React 在您的代码返回后执行这三个更新,因此它们只触发一个 re-render,并且在此期间 re-render isReady
具有与上次相同的值检查了对 useEffect
的依赖性,因此 useEffect
回调不会 运行.
使用 承诺,您的代码将执行此操作:
- 排队状态更新,将
isReady
更改为 false
。
- 退还等待承诺结算。
- 承诺一旦确定:
- 排队添加到
counter
的状态更新。
- 排队状态更新,将
isReady
更改为 true
。
在该列表的第 2 和第 3 之间,React 有机会处理状态更改,它确实如此——当 isReady
为 false
时触发 re-render,这触发您的 useEffect
回调,因为 isReady
的值与上次检查依赖项时的值不同。然后稍后,在 promise 解决后,您的代码将其他几个状态更改排队,这会触发另一个渲染,再次触发您的 useEffect
回调。
重点是:
- 状态更新不是即时的
- 状态更新是批处理
- 即使状态改变然后又变回,如果在这些改变之间没有渲染,由值不同触发的
useEffect
回调不会 运行,因为值检查时没有什么不同
在您提出的评论中:
So I can achieve this behavior without using promise? I need the isReady
flag to control a scroll loader until the function is done handling some data synchronously.
如果处理是 同步,它会占用处理 UI 更新的主线程,并且您的加载指示器很可能不会动画(它肯定会赢如果动画是用 JavaScript 完成的;如果是动画 GIF 或类似的,是否完成取决于浏览器以及是否已经加载了所有帧;如果是 CSS 动画,则快速使用 Chrome 进行测试,Firefox 建议它们至少 将 在处理过程中保持动画,但不能保证)。
如果你能避免在主线程上进行同步处理,我会的。例如,考虑将作品发送到 worker thread。
不过,要做到这一点,您需要更改 isReady
,然后等待开始同步处理,直到 呈现该更改的结果通过等待 useEffect
回调。
这是一个使用我发现的 CSS 微调器的示例 here。
const { useState, useEffect } = React;
const Example = () => {
const [isReady, setIsReady] = useState(true);
// On click, set `isReady` to false
const startProcessing = () => {
setIsReady(false);
};
// When `isReady` becomes false, start "processing".
// (You may want to have an instance variable [via a ref]
// to tell you whether to actually do processing, and if
// so what to process.)
useEffect(() => {
if (!isReady) {
// Start "processing"
const done = Date.now() + 3000;
while (Date.now() < done) {
// Busy wait -- NEVER DO THIS IN REAL-WORLD CODE
}
// Done "processing"
setIsReady(true);
}
}, [isReady]);
return <div>
<input type="button" onClick={startProcessing} value="Start Processing (Takes 3 Seconds)" />
<div>isReady = {String(isReady)}</div>
{isReady ? null : <div className="spinner-loader" />}
</div>;
};
ReactDOM.render(<Example />, document.getElementById("root"));
/*
Credit: jlong @github
Source: https://github.com/jlong/css-spinners
*/
@-moz-keyframes spinner-loader {
0% {
-moz-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes spinner-loader {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spinner-loader {
0% {
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
/* :not(:required) hides this rule from IE9 and below */
.spinner-loader:not(:required) {
-moz-animation: spinner-loader 1500ms infinite linear;
-webkit-animation: spinner-loader 1500ms infinite linear;
animation: spinner-loader 1500ms infinite linear;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
border-radius: 0.5em;
-moz-box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0;
-webkit-box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0;
box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0;
display: inline-block;
font-size: 10px;
width: 1em;
height: 1em;
margin: 1.5em;
overflow: hidden;
text-indent: 100%;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>
我正在使用 React(带有 TypeScript),我发现了非常奇怪的行为。 在方法“handleAnswer”中,我正在更改变量“isReady”并使用 useEffect 挂钩打印值。但是只有当我在那里放置一个计时器时才会发生变化,当我取出计时器时,变量停止变化,并且我不再在控制台中看到输出。我做错了什么?
export const App = () => {
const [isReady, setIsReady] = useState(true);
const [counter, setCounter] = useState(0);
useEffect(() => { console.log(isReady) },[isReady]);
const handleAnswer = async (isLiked: boolean) => {
setIsReady(false);
// WHEN I REMOVE THIS LINE, IT DOESN'T WORK
await new Promise(resolve => setTimeout(resolve, 10));
setCounter(counter + 1)
setIsReady(true);
}
return (
<div className="App" >
{ isReady ? <div className="main-content"></div> :<CircularProgress /> }
<Button variant="contained" onClick={() => handleAnswer(true)} startIcon={<ThumbUpIcon />}>
click
</Button>
</div>
);
}
export default App;
状态更新被排队并在排队它们的代码返回后作为批处理。因此,如果没有承诺,您的 handleAnswer
代码会这样做:
- 排队状态更新,将
isReady
更改为false
。 - 排队添加到
counter
的状态更新。 - 排队状态更新,将
isReady
更改为true
。
React 在您的代码返回后执行这三个更新,因此它们只触发一个 re-render,并且在此期间 re-render isReady
具有与上次相同的值检查了对 useEffect
的依赖性,因此 useEffect
回调不会 运行.
使用 承诺,您的代码将执行此操作:
- 排队状态更新,将
isReady
更改为false
。 - 退还等待承诺结算。
- 承诺一旦确定:
- 排队添加到
counter
的状态更新。 - 排队状态更新,将
isReady
更改为true
。
- 排队添加到
在该列表的第 2 和第 3 之间,React 有机会处理状态更改,它确实如此——当 isReady
为 false
时触发 re-render,这触发您的 useEffect
回调,因为 isReady
的值与上次检查依赖项时的值不同。然后稍后,在 promise 解决后,您的代码将其他几个状态更改排队,这会触发另一个渲染,再次触发您的 useEffect
回调。
重点是:
- 状态更新不是即时的
- 状态更新是批处理
- 即使状态改变然后又变回,如果在这些改变之间没有渲染,由值不同触发的
useEffect
回调不会 运行,因为值检查时没有什么不同
在您提出的评论中:
So I can achieve this behavior without using promise? I need the
isReady
flag to control a scroll loader until the function is done handling some data synchronously.
如果处理是 同步,它会占用处理 UI 更新的主线程,并且您的加载指示器很可能不会动画(它肯定会赢如果动画是用 JavaScript 完成的;如果是动画 GIF 或类似的,是否完成取决于浏览器以及是否已经加载了所有帧;如果是 CSS 动画,则快速使用 Chrome 进行测试,Firefox 建议它们至少 将 在处理过程中保持动画,但不能保证)。
如果你能避免在主线程上进行同步处理,我会的。例如,考虑将作品发送到 worker thread。
不过,要做到这一点,您需要更改 isReady
,然后等待开始同步处理,直到 呈现该更改的结果通过等待 useEffect
回调。
这是一个使用我发现的 CSS 微调器的示例 here。
const { useState, useEffect } = React;
const Example = () => {
const [isReady, setIsReady] = useState(true);
// On click, set `isReady` to false
const startProcessing = () => {
setIsReady(false);
};
// When `isReady` becomes false, start "processing".
// (You may want to have an instance variable [via a ref]
// to tell you whether to actually do processing, and if
// so what to process.)
useEffect(() => {
if (!isReady) {
// Start "processing"
const done = Date.now() + 3000;
while (Date.now() < done) {
// Busy wait -- NEVER DO THIS IN REAL-WORLD CODE
}
// Done "processing"
setIsReady(true);
}
}, [isReady]);
return <div>
<input type="button" onClick={startProcessing} value="Start Processing (Takes 3 Seconds)" />
<div>isReady = {String(isReady)}</div>
{isReady ? null : <div className="spinner-loader" />}
</div>;
};
ReactDOM.render(<Example />, document.getElementById("root"));
/*
Credit: jlong @github
Source: https://github.com/jlong/css-spinners
*/
@-moz-keyframes spinner-loader {
0% {
-moz-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@-webkit-keyframes spinner-loader {
0% {
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
@keyframes spinner-loader {
0% {
-moz-transform: rotate(0deg);
-ms-transform: rotate(0deg);
-webkit-transform: rotate(0deg);
transform: rotate(0deg);
}
100% {
-moz-transform: rotate(360deg);
-ms-transform: rotate(360deg);
-webkit-transform: rotate(360deg);
transform: rotate(360deg);
}
}
/* :not(:required) hides this rule from IE9 and below */
.spinner-loader:not(:required) {
-moz-animation: spinner-loader 1500ms infinite linear;
-webkit-animation: spinner-loader 1500ms infinite linear;
animation: spinner-loader 1500ms infinite linear;
-moz-border-radius: 0.5em;
-webkit-border-radius: 0.5em;
border-radius: 0.5em;
-moz-box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0;
-webkit-box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0;
box-shadow: rgba(0, 0, 51, 0.3) 1.5em 0 0 0, rgba(0, 0, 51, 0.3) 1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) 0 1.5em 0 0, rgba(0, 0, 51, 0.3) -1.1em 1.1em 0 0, rgba(0, 0, 51, 0.3) -1.5em 0 0 0, rgba(0, 0, 51, 0.3) -1.1em -1.1em 0 0, rgba(0, 0, 51, 0.3) 0 -1.5em 0 0, rgba(0, 0, 51, 0.3) 1.1em -1.1em 0 0;
display: inline-block;
font-size: 10px;
width: 1em;
height: 1em;
margin: 1.5em;
overflow: hidden;
text-indent: 100%;
}
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.2/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.2/umd/react-dom.development.js"></script>