1

Why does this not work as a normal one second counter?


function UseEffectBugCounter() {
  const [count, setCount] = React.useState(0);

  React.useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1);
      console.log(count);
    }, 1000);

    return () => clearInterval(intervalId);
  }, []);

  return <div>The count is: {count}</div>;
}

Example: https://codesandbox.io/s/sparkling-rgb-6ebcp

-Is it because of stale closures?

or

-Is it because the count is a state variable and the component would be re-rendered after the state update so a new interval will be created creating some sort of loop?

or

-Is it something else?

I'm looking for a why this occurs in this answer if possible, there are a few different articles stating why it doesn't work (as per above). But none have been able to provide a good argument so far.

5 Answers 5

2

You can use callback for set state to use latest counter value:

setCount(count => (count + 1));
Sign up to request clarification or add additional context in comments.

Comments

0

You may need to add the dependency for count in useEffect. Currently useEffect is only called on the mount and is not called after that (i.e when the count value changes).

So it always says 0 because useEffect is executed only once ( on mount ) and that time the count value is set to 0. And thus it every time it logs 0 on setInterval due to closure.

I have updated the code sandbox to find the reason and meaningful logs. Here is the sandbox link: https://codesandbox.io/s/admiring-thompson-uz2xe

How to find closure value: You can check the logs and traverse through prototype object to find [[Scopes]] and you will get the values as seen in the below screenshot: enter image description here

This would work:

React.useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1);
      console.log(count);
    }, 1000);

    return () => clearInterval(intervalId);
  }, [count]);

You can check this doc: https://reactjs.org/docs/hooks-reference.html#conditionally-firing-an-effect

You can read this as well: You can read this as well: https://overreacted.io/making-setinterval-declarative-with-react-hooks/

Hope this helps!

4 Comments

by adding dependency you're re-running this effect on each render. It's better to use setState callback
Hey this can be done but if you see the logs in setInterval it will still be 0.
I don't think we care about logs, it's just for debugging. If you need this logs, just put console.log into callbask: setCount(count => { console.log(count); return count + 1; });
Hey Andres I know this that it is for debugging, and the callback method is good to get the latest value of the variable. And having a dependency create different setInterval on every render. You may read this blog for a better understanding: overreacted.io/making-setinterval-declarative-with-react-hooks
0

If you removed second params for useEffect your application will be rendered always if u have some change in state, but this is bad practice. You need in second params choose for wich parametrs you need watch ...

Example with choosen params:

React.useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1);
      console.log(count);
    }, 1000);

    return () => clearInterval(intervalId);
  }[count]);

Without

React.useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1);
      console.log(count);
    }, 1000);

    return () => clearInterval(intervalId);
  });

Comments

0

Because you didn't add count to useEffect's dependences, then inside the effect count always is 0.

You should use useReducer to resolve your problem:

function UseEffectBugCounter() {
  const [count, dispatchCount] = React.useReducer((state, { type }) => {
    switch(type) {
      case 'inc':
        return state + 1
      default:
        return state
    }
  }, 0);

  React.useEffect(() => {
    const intervalId = setInterval(() => {
      dispatchCount({ type: 'inc'})
    }, 1000);
    return () => clearInterval(intervalId);
  }, []);
  return <div>The count is: {count}</div>;
}

Comments

0

You need to pass count instead of blank array in useEffect

function UseEffectBugCounter() {
  const [count, setCount] = React.useState(0);
  React.useEffect(() => {
    const intervalId = setInterval(() => {
      setCount(count + 1);
      console.log(count);
    }, 1000);
    return () => clearInterval(intervalId);
  },[count]);
  return <div>The count is: {count}</div>;
}

1 Comment

Hey Vahid, this isn't best method to do this, this will result in the creation of new setInterval on every render, you can check by logging intervalId. My initial thought was the same you can read my answer and check the blog overreacted.io .

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.