5

I'm trying to implement a simple timer that just counts up continuously every second. I'm using react to render the result. The code is only a few lines and makes sense from what I've been reading. It adds and renders properly for the first 6 seconds; however, it just starts displaying random numbers after the 7th or 8th, OR the timer becomes erroneous and updates every random second. My code is as follows, am I doing something wrong? Thanks!

import React, {useState} from 'react';

function App() {

  const [count, setCount] = useState(0); 

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

  return (
    <div className="welcome">
      {count}
    </div>
  );
}

export default App;

sharkeater123
  • 77
  • 1
  • 6
  • You have not posted enough code for anybody to help. What does `setCount()` do, for example? How/when/where is `App()` called? – Pointy Aug 14 '21 at 15:13
  • setCount is the react hook and App() is the react component getting rendered. Sorry if I didn't provide enough info :( – sharkeater123 Aug 14 '21 at 15:17

3 Answers3

6

that because it start a new one every time the component rerender use this instead

import React, {useState} from 'react';

function App() {

  const [count, setCount] = useState(0); 
  const timer = useRef(null);


useEffect(() => {
  timer.current = setInterval(()=>{setCount(count + 1)}, 1000);
  return () => {
    if(timer.current !== null) clearInterval(timer.current);
  };
}, []);

  return (
    <div className="welcome">
      {count}
    </div>
  );
}

export default App;


Omar Khaled
  • 439
  • 1
  • 4
  • 16
3

Your code has some issues, as do the other answers. They can be summarized in three points:

  1. If you want to use Interval or Timeout (or any other event listener), you need to do this inside a useEffect, so you won't add a new listener in each render.

  2. If you use an useEffect for it, you'll need to clean up the Interval/Timeout (or any other listener) to avoid memory leaks.

  3. We can avoid using the count variable inside the dependencies array by using setCount(oldCount => oldCount + 1). Citing React documentation:

If the new state is computed using the previous state, you can pass a function to setState. The function will receive the previous value, and return an updated value.

If you ignore step 3 by using count as a hook-dependency, you will create and clean up an Interval in each render - this is unnecessary.
If you decide to just use setCount(count + 1) without adding it to the dependency array, you're using hooks wrong, count will always have the same value (the initial one).

I added a button to force a re-render in the example below to show that the count keeps going as expected.

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

  React.useEffect(() => {
    // Store the interval id in a const, so you can cleanup later
    const intervalId = setInterval(() => {
      setCount(oldCount => oldCount + 1);
    }, 1000);
    
    return () => {
      // Since useEffect dependency array is empty, this will be called only on unmount
      clearInterval(intervalId);
    };
  }, []);
  
  function forceRerender() {
    setForced(oldForced => oldForced + 1);
  }

  return (
    <div>
      <p>{count} seconds</p>
      <p>Forced re-renders: {forced}</p>
      <button onClick={forceRerender}>Force a re-render</button>
    </div>
  );
}

ReactDOM.render(<App />, document.querySelector('#app'));
<div id="app"></div>
<script crossorigin src="https://unpkg.com/react@17/umd/react.production.min.js"></script>
<script crossorigin src="https://unpkg.com/react-dom@17/umd/react-dom.production.min.js"></script>
Rafael Tavares
  • 5,678
  • 4
  • 32
  • 48
1

The setInterval call should be put inside a useEffect hook and cleanup should be done too:

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

function App() {
  const [count, setCount] = useState(0);
  const ref = useRef();

  useEffect(() => {
    ref.current = setInterval(() => {
      setCount(count + 1);
    }, 1000);
    return () => {
      clearInterval(ref.current);
    };
  }, [count]);

  return <div className="welcome">{count}</div>;
}

export default App;
silviubogan
  • 3,343
  • 3
  • 31
  • 57
  • 2
    doesn't this restart the `setInterval` everytime `count` increments? Shouldn't the arguments array be empty? – D.B.K Aug 14 '21 at 15:27
  • 1
    Yea it seems like, whatever React stuff is going on, the main problem is that the `setInterval()` call is being made over and over again and nothing stops previously-started interval timers. – Pointy Aug 14 '21 at 15:28
  • I've seen that the other answer does not work, although I do not know why. I've expected it to work better than my one. – silviubogan Aug 14 '21 at 15:32
  • It worked! The problem with what I did was calling setInterval everytime the screen rerendered which caused the erroneous behavious. – sharkeater123 Aug 14 '21 at 16:52