【问题标题】:Updating state in useEffect after every second for 10 seconds每秒更新 useEffect 中的状态 10 秒
【发布时间】:2021-08-29 22:06:35
【问题描述】:

通过不进入实际使用,我创建了一个简单的示例来解释我想要做什么。

我有一个状态对象{num:0},我想在每秒 10 秒后更新 num,据此,我创建了一个运行良好的类组件。

class App extends React.Component {
  constructor() {
    super();
    this.state = {
      num: 0
    };
  }

  componentDidMount = () => {
    for (let i = 0; i < 10; i++) {
      setTimeout(() => this.setState({ num: this.state.num + 1 }), i * 1000);
    }
  };

  render() {
    return (
      <>
        <p>hello</p>
        <p>{this.state.num}</p>
      </>
    );
  }
}

现在我想在功能组件中复制相同的功能,但我做不到。我试过如下图:

const App = () => {
  const [state, setState] = React.useState({ num: 0 });

  React.useEffect(() => {
    for (let i = 0; i < 10; i++) {
      setTimeout(() => setState({ num: state.num + 1 }), i * 1000);
    }
  }, []);

  return (
    <>
      <p>hello</p>
      <p>{state.num}</p>
    </>
  );
};

谁能帮我解决我在这里做错了什么?

【问题讨论】:

标签: javascript reactjs react-hooks settimeout use-effect


【解决方案1】:

你所有的超时确实运行,但是因为你在第一次渲染时设置了它们,你已经创建了一个围绕初始 state.num 值的闭包,所以当每个超时都会设置新的状态值到 0 + 1 并且没有任何变化。

评论中提到的副本涵盖了详细信息,但这里有一个快速工作的 sn-p,使用 ref 作为计数器,在 10 次迭代后停止并在 useEffect 返回时清理计时器。

const App = () => {
  const [state, setState] = React.useState({ num: 0 });
  const counter = React.useRef(0);
  
  React.useEffect(() => {
    if (counter.current < 10) {
      counter.current += 1;
      const timer = setTimeout(() => setState({ num: state.num + 1 }), 1000);

      return () => clearTimeout(timer);
    }
  }, [state]);

  return (
    <div>
      <p>hello</p>
      <p>{state.num}</p>
    </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() 调用以避免创建闭包,但是您放弃了通过设置新的超时来允许的精细控制每次渲染。

const App = () => {
  const [state, setState] = React.useState({ num: 0 });

  React.useEffect(() => {
    for (let i = 0; i < 10; i++) {
      setTimeout(() => setState(prevState => ({ ...prevState, num: prevState.num + 1 })), i * 1000);
    }
  }, []);

  return (
    <div>
      <p>hello</p>
      <p>{state.num}</p>
    </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>

【讨论】:

    【解决方案2】:

    当你创建一个钩子时,它会捕获钩子创建时的状态(即它创建一个闭包)。 state.num 在创建定时器时的值为 0,因此每次超时都会将状态设置为 0 + 1

    针对您的特定问题最简单的解决方法是 use the other version of setState which allows you to pass a callback,它会根据之前的值修改状态:

    React.useEffect(() => {
      for (let i = 0; i < 10; i++) {
        setTimeout(() => setState(prev => ({ num: prev.num + 1 })), i * 1000);
      }
    }, []);
    

    这样,您不会在创建钩子时捕获状态值。

    正如其中一个 cmets 所指出的 - 如果您的组件卸载,您还应该从挂钩返回一个清理函数以停止计时器 - 但这超出了您的问题范围。

    【讨论】:

      【解决方案3】:

      要解决这个问题,您必须使用“i”的值而不是 state.num 值。如果您使用 state.num 值,它将始终为 0 而不是当前值,因为您每次都在 useEffect 挂钩中重新初始化它。在下面找到更正的代码,现在它可以工作了。

      import React from "react";
      const App = () => {
        const [state, setState] = React.useState({ num: 0 });
      
        React.useEffect(() => {
          for (let i = 0; i < 10; i++) {
            setTimeout(() => setState({ num: i + 1 }), i * 1000);
          }
        }, []);
      
        return (
          <>
            <p>hello</p>
            <p>{state.num}</p>
          </>
        );
      };
      
      export default App;
      
      

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2014-06-20
        • 2012-06-18
        • 2022-10-04
        • 2021-01-13
        • 1970-01-01
        • 2013-01-26
        • 1970-01-01
        相关资源
        最近更新 更多