nonopolarity
nonopolarity

Reputation: 151214

Why does a count using React Hooks useState() alternate between two numbers?

The following is a React Hooks experiment of using useState(). It works fine except when the + button was clicked on, then the number can be alternating from 7001 and 7000 and then flashing between some numbers quickly.

Actually, without clicking on the +, the number behaved well but up to about 8000 or 9000, then it might start to flash between some numbers. Why is that and how can it be fixed?

P.S. initial debugging finding was that: it seems Counter() was called multiple times, setting up an Interval Timer every time. So "magically", it seems the useState() ran only once -- for some unknown and magical reason -- or maybe it ran more than once but just returned the exact same content each time, for some magical mechanism. The initial value of 0 really was so for the first time. When it was useState(0) for future times, the count was not 0... we wouldn't want that, but then it wasn't that functional (as in a math function) either.

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

    setInterval(() => {
        setCount(count + 1000);
    }, 1000);

    return (
        <div>
            <button onClick={() => setCount(count + 1)}> + </button>                    
            { count }
            <button onClick={() => setCount(count - 1)}> - </button> 
        </div>
    );
}

ReactDOM.render(<Counter />, document.querySelector("#root"));
button { margin: 0 1em }
<script src="https://unpkg.com/react@16/umd/react.development.js" crossorigin></script>
<script src="https://unpkg.com/react-dom@16/umd/react-dom.development.js" crossorigin></script>

<div id="root"></div>

Upvotes: 0

Views: 3007

Answers (2)

Gennady Dogaev
Gennady Dogaev

Reputation: 5991

  1. Code in functional component gets executed every time when component is re-rendered. So, on each re-render you are starting an infinite timer that adds 1000 to counter each second
  2. Each time you change component's state, React re-renders it. Meaning, every execution of setCount leads to new re-render and new timer is started
  3. Also, setCount is asynchronous and if you need to rely on previous state to determine next one, you should call with callback, it like demonstrated in other answer (setCount(c => c + 1))

Something like this is supposed to work:

import React, {useState, useRef, useEffect} from 'react';

function Counter() {
    const [count, setCount] = useState(0);
    //useRef gives us an object to store things between re-renders
    const timer = useRef();

    useEffect(() => {
      timer.current = setInterval(() => {
        setCount(count => count + 1000);
      }, 1000);

      //If we return a function, it will be called when component is dismounted
      return () => {
        clearInterval(timer.current);
      }
    }, []);

    return (
        <div>
            <button onClick={() => setCount(count => count + 1)}> + </button>                    
            { count }
            <button onClick={() => setCount(count => count - 1)}> - </button> 
        </div>
    );
}

Upvotes: 6

Michael
Michael

Reputation: 958

Not quite sure about the 'why is that' but it's fixed with substituting setCount(c => c + 1) in the buttons and setCount(c => c + 1000) in the interval.

Putting the 'setInterval' in an effect also makes sure that there is only one interval...

  React.useEffect(() => {  
     setInterval(() => {
        setCount(c => c + 1000);
     }, 1000);
  },[])

PS Counter() gets called on every render, I think... while useState only gets called once per mounting by design.

Upvotes: 1

Related Questions