Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to Setup a setInterval Timer Properly in a React Functional Component?

I have just started learning react and I was watching a tutorial which deals with state and hooks. It just deals with updating time every 1000 milliseconds (or so I thought).

import React from "react";
let count = 0;

function App() {
  const now = new Date().toLocaleTimeString();
  let [time, setTime] = React.useState(now);


  function updateTime(){
    const newTime = new Date().toLocaleTimeString();
    setTime(newTime);
    count++;
    console.log(count);
    console.log(new Date().getMilliseconds());
  }

  setInterval(updateTime, 1000);


  return (
    <div className="container">
      <h1>{time}</h1>
      <button onClick = {updateTime}>time</button>
    </div>
  );
}

export default App;

The purpose of the tutorial was just a simple example on how to update time, but what I noticed is that it is updated multiple times (in bursts) every 1000 milliseconds. I am suspecting that each time change to a hook happens new component is rendered but the old component is still there updating and spawning more components resulting in what seems like exponential growth of calls every 1000 milliseconds.

I am very curious what is happening here? How would I go about lets say having a simple counter that updates every 1000 milliseconds? setTime(count) obviously does not work

like image 767
B.James Avatar asked Aug 10 '20 03:08

B.James


2 Answers

The issue: in your current implementation, setInterval would be called every time the component renders (i.e., will also be called after the time state is set) and will create a new interval - which produced this "exponential growth" as seen in your console.

As explained in the comments section: useEffect would be the best way to handle this scenario when dealing with functional components in React. Take a look at my example below. useEffect here will only run after the initial component render (when the component mounts).

React.useEffect(() => {
  console.log(`initializing interval`);
  const interval = setInterval(() => {
    updateTime();
  }, 1000);

  return () => {
    console.log(`clearing interval`);
    clearInterval(interval);
  };
}, []); // has no dependency - this will be called on-component-mount

If you want to run an effect and clean it up only once (on mount and unmount), you can pass an empty array ([]) as a second argument. This tells React that your effect doesn’t depend on any values from props or state, so it never needs to re-run.

In your scenario, this is a perfect usage of the "empty array as a second argument" since you would only need to set the interval when the component is mounted and clear the interval when it unmounts. Take a look at the function that the useEffect returns as well. This is our cleanup function that will run when the component unmounts. This will "clean" or in this case, clear the interval when the component is no longer in use.

I've written a small application that demonstrates everything I've covered in this answer: https://codesandbox.io/s/so-react-useeffect-component-clean-up-rgxm0?file=/src/App.js

I've incorporated a small routing functionality so that "unmounting" of the component can be observed.


My old answer (not recommended):

A new interval is created everytime the component re-renders, which is what happens when you set the new state for time. What I would do is clear the previous interval (clearInterval) before setting up a new one

try {
  clearInterval(window.interval)
} catch (e) {
  console.log(`interval not initialized yet`);
}

window.interval = setInterval(updateTime, 1000);

https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setInterval

like image 152
95faf8e76605e973 Avatar answered Sep 23 '22 15:09

95faf8e76605e973


As Macro Amorim answered, useEffect is the best way to do this. Here the code:

useEffect(() => {
    const interval = setInterval(() => {
         const newTime = new Date().toLocaleTimeString();
         setTime(newTime);
    }, 1000)

    return () => {
        clearInterval(interval);
    }
}, [time])
like image 23
Arnas Avatar answered Sep 19 '22 15:09

Arnas