B.James
B.James

Reputation: 196

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

Upvotes: 14

Views: 25932

Answers (6)

ClassY
ClassY

Reputation: 655

None of the answers above filled my requirements. What if I wanted to pass the timer from the parent component? What if countdown timer value was a state? The accpeted answer starts the interval as soon as it is rendered which might bad in many scenarios! The user of that Countdown component must unmount the Countdown component to run the cleanup code which means if we never unmount that component (by conditional rendering or whatever) we might never endup calling the clean up code which is a big flaw.

my solution to these problems are as follows:

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

export type Props = {
  seconds: number;
  decreaseCountDown: () => void;
};

export const MIN_TIMER_VALUE = -1;

const CountDownTimer = ({ seconds, decreaseCountDown }: Props) => {
  const intervalId = useRef<NodeJS.Timer | undefined>(undefined);

  useEffect(() => {
    createIntervalIfRequired();
    return () => clearIntervalIfRequired();
  }, [seconds]);

  const createIntervalIfRequired = () => {
    if (intervalId.current !== undefined) {
      return;
    }
    if (seconds <= MIN_TIMER_VALUE) {
      return;
    }
    const createdIntervalId = setInterval(() => {
      decreaseCountDown();
    }, 1000);

    intervalId.current = createdIntervalId;
  };
  
  const clearIntervalIfRequired = () => {
    if (seconds > MIN_TIMER_VALUE || intervalId.current == undefined) {
      return;
    }
    clearInterval(intervalId.current);
    intervalId.current = undefined;
  };

  return (
    <span className={`px-2 ${seconds <= MIN_TIMER_VALUE ? "hidden" : ""}`}>
      {beautifyTime(seconds)}
    </span>
  );
};

const beautifyTime = (time: number): string => {
  let minutes = parseInt((time / 60).toString()).toString();
  let seconds = parseInt((time % 60).toString()).toString();

  if (seconds.length == 1) {
    seconds = "0" + seconds;
  }

  if (minutes.length == 1) {
    minutes = "0" + minutes;
  }

  return `${minutes}:${seconds}`;
};

export default CountDownTimer;

This way we create the interval only if the props seconds is bigger than 0 and we clean up the interval even if our component is not unmounted! We will only start a new interval if we don't have any intervals spinning up right now.

The consumer of this component will do something like this:

      <CountDownTimer
        seconds={countDownTimer}
        decreaseCountDown={() => {
          setCountDownTimer((currentCounter) => currentCounter - 1);
        }}
      />

This way the comsumer does not care about dismounting the component or how many intervals are created. All it knows is what the curret seconds value is.

even on react.dev docs they used useRef hook for intervalId.

Upvotes: 1

Dadip Bhattarai
Dadip Bhattarai

Reputation: 1

Simply Timer using Hooks

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

const TimeHeader = () => {
  const [timer, setTimer] = useState(new Date());

  useEffect(() => {
    const interval = setInterval(() => {
      setTimer(new Date());
    }, 1000);
  }, []);

  return (
    <div className="electonTimer">
      <div className="electionDate">
        <h1>{timer.toLocaleTimeString()}</h1>
      </div>
    </div>
  );
};

Upvotes: 0

Kirill Tereshchenko
Kirill Tereshchenko

Reputation: 31

You can also try this -

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

export default function App() {
  const [timer, setTimer] = useState(0);
  const [toggle, setToggle] = useState(false);

  useEffect(() => {
    let counter;
    if (toggle) {
      counter = setInterval(() => setTimer(timer => timer + 1), 1000);
    }
    return () => {
      clearInterval(counter);
    };
  }, [toggle]);

  const handleStart = () => {
    setToggle(true);
  };

  const handleStop = () => {
    setToggle(false);
  };

  const handleReset = () => {
    setTimer(0);
    setToggle(false);
  };

  return (
    <div>
      <h1>Hello StackBlitz!</h1>
      <p>Current timer - {timer}</p>
      <br />
      <button onClick={handleStart}>Start</button>
      <button onClick={handleReset}>Reset</button>
      <button onClick={handleStop}>Stop</button>
    </div>
  );
}

Upvotes: 3

95faf8e76605e973
95faf8e76605e973

Reputation: 14191

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

Upvotes: 21

Arnas
Arnas

Reputation: 662

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])

Upvotes: 6

Marco Amorim
Marco Amorim

Reputation: 33

In this situation you could use the useEffect hook from React, and on the return inside the function you can use the clearInterval function, I recommend you to look it up, useEffect is a great fit for what you want to do.

Upvotes: 0

Related Questions