React -设置状态只在异步操作中工作



我正在使用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代码会这样做:

  1. 队列状态更新将isReady更改为false
  2. 队列状态更新添加到counter
  3. 队列状态更新更改isReadytrue

React做这三个更新一旦你的代码已经返回,所以他们只触发一个单一的重新渲染,在重新渲染isReady有相同的值,它有最后一次依赖于你的useEffect被检查,所以useEffect回调不运行。

使用作为承诺,你的代码会这样做:

  1. 队列状态更新将isReady更改为false
  2. 返回等待承诺解决。
  3. 一旦承诺解决:
    1. 队列状态更新添加到counter
    2. 队列状态更新更改isReadytrue

在列表中的#2和#3之间,React有机会处理状态变化,所以它确实这样做了——当isReadyfalse时触发重新渲染,这触发你的useEffect回调,因为isReady的值与上次检查依赖项的值不同。然后,在承诺解决后,您的代码排队其他几个状态变化,触发另一个渲染,再次触发useEffect回调。

关键是:

  • 状态更新不是立即的
  • 状态更新批处理
  • 即使状态改变了,然后又改变了,如果在这些变化之间没有渲染,由值不同触发的useEffect回调不会运行,因为值在检查时没有不同

在你的评论中问:

所以我可以实现这个行为不使用承诺?我需要isReady标志来控制滚动加载器,直到函数完成同步处理一些数据。

如果处理是同步的,它将占用处理UI更新的主线程,并且你的加载指示器很可能不会被动画化(如果动画是用JavaScript完成的,它肯定不会;如果它是GIF动画或类似的,它是否完成取决于浏览器以及所有帧是否已经加载;如果它是一个CSS动画,在Chrome和Firefox上的快速测试表明,它们至少会在处理发生时保持动画,但不能保证)。

如果可以避免主线程上的同步处理,我就会这样做。例如,考虑将工作发送给工作线程。

但是,要做到这一点,您需要更改isReady,然后等待开始同步处理,直到之后,通过等待useEffect回调呈现更改的结果。

下面是我在这里找到的一个使用CSS旋转器的例子。

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>

最新更新