Reputation: 196
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
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
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
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
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
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
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