Hypothesis
Hypothesis

Reputation: 1379

React cannot handle counter properly

I have a simple code which iterates through an array and logs them in the interval of 1000 ms as such :

  const arr = [1, 2, 3];
  let i = 0;
  const choice = () => {
    const interval = setInterval(() => {
      console.log(arr[i++ % arr.length]);
      if (i === 8) {
        clearInterval(interval);
      }
    }, 1000);
  };

  choice();

Upon introducing React state into the code, the whole thing goes absolutely mad and starts counting out of the interval to a point that I reach almost infinite loop although the simple console.log instead of react state works fine.

  const [ele, setEle] = React.useState(null);
  const arr = [1, 2, 3];
  let i = 0;
  const choice = () => {
    const interval = setInterval(() => {
      setEle(arr[i++ % arr.length]);
      if (i === 8) {
        clearInterval(interval);
      }
    }, 1000);
  };

  choice();

return( 
    <h1>{ele}</h1>
)

I wonder how I can achieve the effect using the state with the current code.

https://codesandbox.io/s/condescending-dubinsky-kbl5m

Upvotes: 1

Views: 73

Answers (2)

possum
possum

Reputation: 2916

I found the other answer somewhat unsatisfying because it requires a new timeout to be generated each time. It is possible to set up a single setInterval which requires a useRef to get an up-to-date setI into the timer. The empty useEffect dependencies ensures the setInterval is not re-run on each state update.

const App = () => {
  const [i, setI] = React.useState(0);
  const timer = React.useRef();

  timer.current = () => setI(i+1);

  React.useEffect(() => {
    const interval = setInterval(() => {
      timer.current();
    }, 1000);
    return () => {
      clearInterval(interval)
    };
  }, [])

  return <div>{i}</div>;
}

Upvotes: 0

CertainPerformance
CertainPerformance

Reputation: 370589

With your current implementation, every render of the component is initializing a new interval, so you get lots of intervals running simultaneously, each of which initiate even more intervals. Use setTimeout instead, so that every render only initializes exactly one action to take place in the future.

Since it's the index of the array that changes each time, consider using that as state instead:

const App = () => {
  const [i, setI] = React.useState(0);
  const arr = [1, 2, 3];
  if (i !== 8) {
    setTimeout(() => {
      setI(i + 1);
    }, 1000);
  }

  return (
    <div className="App">
      <h1>{arr[i % arr.length]}</h1>
    </div>
  );
}

ReactDOM.render(
  <App />,
  document.body
);
<script crossorigin src="https://unpkg.com/react@16/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@16/umd/react-dom.development.js"></script>

Upvotes: 4

Related Questions