1

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?

1

3 Answers 3

7

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>

0

When you create a hook, it captures the state at the time the hook was created (i.e. it create a closure). The value of state.num at the time you create the timers is 0, so each of the timeouts will setting the state to 0 + 1.

The easiest fix for your specific problem is to use the other version of setState which allows you to pass a callback which modifies the state based on its previous value:

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

That way, you're not capturing the value of the state at the time the hook is created.

As pointed out in one of the comments - you should also be returning a cleanup function from the hook to stop the timers if your component dismounts - but that's outside the scope of your question.

-1

To solve this, you have to use the value of "i" instead of the state.num value. If you use the state.num value, it will always be 0 instead of the current value as you are reinitializing it every time in the useEffect hook. Find below the corrected code and now it works.

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;

Not the answer you're looking for? Browse other questions tagged or ask your own question.