Search code examples
javascriptreactjsreact-hookssettimeoutuse-effect

Updating state in useEffect after every second for 10 seconds


By not going into the actual usage, I have created a simple example to explain what I want to do.

I have a state object {num:0} and I want to update the num after every second for 10 seconds and according to that, I created a class component that is working perfectly fine.

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>
      </>
    );
  }
}

Now I want to replicate the same functionality in the functional component but I am unable to. I tried as shown below:

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>
    </>
  );
};

Can anyone please help me with what I am doing wrong here?


Solution

  • All of your timeouts do run, but because you set them all on the first render you've created a closure around the initial state.num value, so when each one fires it sets the new state value to 0 + 1 and nothing changes.

    The duplicate noted in the comment covers the details, but here's a quick working snippet using a ref as a counter to stop at after 10 iterations and cleaning up the timer in the return of the 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>

    To make your code work as it is, setting all of them at once you can pass a callback to the setState() call in order to avoid creating a closure, but you give up the granular control allowed by setting new timeouts on each render.

    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>