clearInterval() fails to stop an interval running on a timer [duplicate] - javascript

This question already has answers here:
clearInterval not working in React Application using functional component
(5 answers)
Closed 12 days ago.
First time using clearInterval() looking at other examples and the interval docs this appears to be the way to stop an interval. Not sure what I am missing.
The intention is to kill the timer when the currentStop prop updates.
import React, { useEffect, useState } from 'react';
type Props = {
stopNumber: number;
currentStop: number;
};
const timerComponent = ({ stopNumber, currentStop }: Props) => {
let interval: NodeJS.Timer;
// Update elapsed on a timer
useEffect(() => {
if (stopNumber === currentStop) {
interval = setInterval(() => {
console.log('timer is running');
}, 3000);
// Clear interval on unmount
return () => clearInterval(interval);
}
}, []);
// Clear timers that were running
useEffect(() => {
if (stopNumber !== currentStop) {
clearInterval(interval);
}
}, [currentStop]);
};

Store the intervalId on a ref instead
const timerComponent = ({ stopNumber, currentStop }: Props) => {
const intervalRef = useRef({
intervalId: 0
})
// Update elapsed on a timer
useEffect(() => {
if (stopNumber === currentStop) {
intervalRef.current.intervalId = setInterval(() => {
console.log('timer is running');
}, 3000);
// Clear interval on unmount
return () => clearInterval(intervalRef.current.intervalId);
}
}, []);
// Clear timers that were running
useEffect(() => {
if (stopNumber !== currentStop) {
clearInterval(intervalRef.current.intervalId);
}
}, [currentStop]);
};

Use a ref to store the interval id instead.
let interval = useRef();
// to start the setInterval:
interval.current = setInterval(...);
// to stop the setInterval:
clearInterval(interval.current);

Related

Why is clearInterval() not clearing my timer variable and what would be the best way to go about this? react.js [duplicate]

From what I can tell, the timer being called in a different scope.. how can I accomplish a function to stop the timer? Going a bit crazy here, thank you for any help.
const SomeComponent = ({ isPlaying }) => {
let timer = null;
React.useEffect(() => {
if (isPlaying) {
startTimer();
}
},[isPlaying]);
const startTimer = () => {
timer = setInterval(() => {
console.log('tick');
}, 1000);
};
const stopTimer = () => {
console.log('stopping timer: ', timer); // shows null, instead of giving the timerId to stop properly
clearInterval(timer);
};
The timer variable will "reset" each time your component is re-rendered. Even if it holds your timer, a re-render will set its value to null again.
You could either move out of the component scope, or use useRef to keep the variable through re-renders:
const SomeComponent = ({ isPlaying }) => {
const timer = React.useRef(null);
React.useEffect(() => {
if (isPlaying) {
startTimer();
}
return () => clearInterval(timer.current);
}, [isPlaying]);
const startTimer = () => {
timer.current = setInterval(() => {
console.log('tick');
}, 1000);
};
const stopTimer = () => {
clearInterval(timer.current);
};
Note that I also force a clearInterval by using a return inside the useEffect. This way the component will automatically "clean up" when it unmounts. I also changed timer to be a constant.

React, variable speed Clock with setTimeout

goal: create a Clock component which calls a callback method at regular intervals, but whose speed can be controlled.
Tricky part: do not reset the clock timer immediately when the speed changes, but at the next "tick" check the desired speed and if it has changes, reset the current interval and schedule a new one. This is needed to keep the clock ticket at a smooth pace when changing the speed.
I thought that passing a function getDelay that returns the delay (instead of the value of the delay itself) would make this work, but it doesn't.
If I let useEffect track the getDelay function it will reset when the delay changes. If it don't track getDelay the speed will not change while the clock is running.
import React, { useEffect, useRef } from "react";
type Callback = () => void;
function useInterval(tickCallback: Callback, getDelay: () => number, isPlaying: boolean) {
const refDelay = useRef<number>(getDelay());
useEffect(() => {
let id: number;
console.log(`run useEffects`);
function tick() {
const newDelay = getDelay();
if (tickCallback) {
console.log(`newDelay: ${newDelay}`);
tickCallback();
if (newDelay !== refDelay.current) {
// if delay has changed, clear and schedule new interval
console.log(`delay changed. was ${refDelay.current} now is ${newDelay}`)
refDelay.current = newDelay;
clear();
playAndSchedule(newDelay);
}
}
}
/** clear interval, if any */
function clear() {
if (id) {
console.log(`clear ${id}`)
clearInterval(id);
}
}
/** schedule interval and return cleanup function */
function playAndSchedule(delay: number) {
if (isPlaying) {
id = window.setInterval(tick, delay);
console.log(`schedule delay id ${id}. ms ${delay}`)
return clear
}
}
return playAndSchedule(refDelay.current);
},
// with getDelay here the clock is reset as soon as the delay value changes
[isPlaying, getDelay]);
}
type ClockProps = {
/** true if playing */
isPlaying: boolean;
/** return the current notes per minute */
getNpm: () => number;
/** function to be executed every tick */
callback: () => void;
}
export function Clock(props: ClockProps) {
const { isPlaying, getNpm, callback } = props;
useInterval(
callback,
() => {
console.log(`compute delay for npm ${getNpm()}`);
return 60_000 / getNpm();
},
isPlaying);
return (<React.Fragment />);
}
you can use something like this:
import React, { useCallback, useEffect, useMemo, useRef } from 'react';
function useInterval(tickCallback: () => void, delay: number, isPlaying: boolean) {
const timeout = useRef<any>(null);
const savedDelay = useRef(delay);
const savedTickCallback = useRef(tickCallback);
useEffect(() => {
savedDelay.current = delay;
}, [delay])
useEffect(() => {
savedTickCallback.current = tickCallback;
}, [tickCallback])
const startTimeout = useCallback(() => {
const delay = savedDelay.current;
console.log('next delay', delay);
timeout.current = setTimeout(() => {
console.log('delay done', delay);
savedTickCallback.current();
startTimeout();
}, savedDelay.current);
}, []);
useEffect(() => {
if (isPlaying) {
if (!timeout.current) {
startTimeout();
}
} else {
if (timeout.current) {
clearTimeout(timeout.current);
}
}
},
[isPlaying, startTimeout],
);
}
type ClockProps = {
/** true if playing */
isPlaying: boolean;
/** return the current notes per minute */
getNpm: () => number;
/** function to be executed every tick */
callback: () => void;
}
export const Clock: React.FC<ClockProps> = ({ isPlaying, getNpm, callback }) => {
const delay = useMemo(() => {
console.log(`compute delay for npm ${getNpm()}`);
return 60_000 / getNpm();
}, [getNpm]);
useInterval(callback, delay, isPlaying);
return null;
};

React: Use Interval Not Clearing

I am trying to clear the useInterval function once the joke array has 5 jokes as objects. However, I'm not sure what I'm doing wrong. For full code: https://codesandbox.io/s/asynchronous-test-mp2fq?file=/AutoComplete.js
const [joke, setJoke] = React.useState([]);
function useInterval(callback, delay) {
const savedCallback = useRef();
let id;
useEffect(() => {
savedCallback.current = callback;
if (joke.length === 5) {
console.log("5 STORED AND CLEARED INTERVAL");
return () => clearInterval(id);
}
});
useEffect(() => {
function tick() {
savedCallback.current();
}
id = setInterval(tick, delay);
return () => clearInterval(id);
}, [delay]);
}
useInterval(() => {
// setJoke(joke);
axios.get("https://api.chucknorris.io/jokes/random").then((res) => {
setJoke(joke.concat(res.data.value));
console.log("JOKE: ", joke);
});
console.log("Every 5 seconds");
}, 5000);
Use a ref to store the interval id, to preserve it between re-renders. When the length is 5, call clearInterval instead of returning a clean function that would be called when the component unmounts.
In addition, make the hook agnostic of the actual stop condition by supplying a stop function, and calling it whenever the component re-renders.
function useInterval(callback, delay, stop) {
const savedCallback = useRef();
const interval = useRef();
useEffect(() => {
savedCallback.current = callback;
if (stop?.()) { // call stop to check if you need to clear the interval
clearInterval(interval.current); // call clearInterval
}
});
useEffect(() => {
function tick() {
savedCallback.current();
}
interval.current = setInterval(tick, delay); // set the current interval id to the ref
return () => clearInterval(interval.current);
}, [delay]);
}
const Example () => {
const [joke, setJoke] = React.useState([]);
useInterval(() => {
// setJoke(joke);
axios.get("https://api.chucknorris.io/jokes/random").then((res) => {
setJoke(joke.concat(res.data.value));
console.log("JOKE: ", joke);
});
console.log("Every 5 seconds");
}, 5000, () => joke.length > 4);
return (
...
);
};
Try adding in callback to your dependency array
useEffect(() => {
savedCallback.current = callback;
if (joke.length === 5) {
console.log("5 STORED AND CLEARED INTERVAL");
return () => clearInterval(id);
}
},[callback]);

Timer/Counter for react component - value remains 0 after increasing it with setInterval()

export default function Timer() {
const [timer, setTimer] = useState(0)
const checkTimer = () => {
console.log(timer);
}
useEffect(() => {
const timer = setInterval(() => {
setTimer(prevCount => prevCount + 1);
}, 1000);
startProgram(); //This starts some other functions
return () => {
checkTimer();
clearInterval(timer);
}
}, [])
}
Above is a simplified version of my code and the main issue - I am trying to increase the timer state by setting an interval in useEffect() (only once). However, in checkTimer() the value is always 0, even though the console statement execute every second. I am new to reactjs and would appreciate some help as this is already taking me too many hours to fix.
checkTimer is showing you the initial value of timer state because of stale closure. That means at the time when useEffect was executed, (i.e. once at component mount due to [] as dependency), it registered a cleanup function which created a closure around checkTimer function (and everything, state or props values, it uses). And when this closure was created the value of timer state was 0. And it will always remain that.
There are few options to fix it.
One quick solution would be to use useRef:
const timer = useRef(0);
const checkTimer = () => {
console.log(timer.current);
};
useEffect(() => {
const id = setInterval(() => {
timer.current++;
}, 1000);
return () => {
checkTimer();
clearInterval(id);
};
}, []);
Check this related post to see more code examples to fix this.
Edit:
And, if you want to show the timer at UI as well, we need to use state as we know that "ref" data won't update at UI. So, the option 2 is to use "updater" form of setTimer to read the latest state data in checkTimer function:
const [timer, setTimer] = useState(0);
const checkTimer = () => {
let prevTimer = 0;
setTimer((prev) => {
prevTimer = prev; // HERE
return prev; // Returning same value won't change state data and won't causes a re-render
});
console.log(prevTimer);
};
useEffect(() => {
const id = setInterval(() => {
setTimer((prev) => prev + 1);
}, 1000);
return () => {
checkTimer();
clearInterval(id);
};
}, []);

Why is my React setState is looping forever?

I am trying to make a pomodoro app and the count down clock will keep toggle the breaking state when the time is up, the breaking state will indicate whether you're currently working (breaking === false) or you're taking a break (breaking === true).
However, the console shows that the setBreaking are keep looping, resulting in error. I've tried to pass in an anonymous function with prevState => !prevState, error still occur. Any advice?
Here are the excerpt:
function Clock() {
const [minute, setMinute] = useState(parseInt(remainingTime/60));
const [second, setSecond] = useState(padZero(remainingTime%60));
const [breaking, setBreaking] = useState(false);
function padZero(num) {
return num.toString().padStart(2,0);
}
function countDown() {
useInterval(() => {
setRemainingTime(remainingTime-1);
}, 1000);
}
if (remainingTime === 0) {
setBreaking(!breaking) // keep looping
setCountDown(false);
setRemainingTime(breakMinute*60)
}
if (countingDown === true) {
countDown();
} else {
console.log('Timer stopped!');
}
return <h1>{minute}:{second}</h1>
};
You are not supposed to put subscriptions, timers... inside the main body. Instead, you need to put your code and start the countdown into a useEffect(..., []) hook.
By not using hooks, your code will be executed everytime you are trying to render the component, and sometimes it's kind of random...
function Clock() {
const [minute, setMinute] = useState(parseInt(remainingTime/60));
const [second, setSecond] = useState(padZero(remainingTime%60));
const [breaking, setBreaking] = useState(false);
React.useEffect(() => {
// Your code here
}, []);
return <h1>{minute}:{second}</h1>
};
Here you go with a solution using useEffect
function Clock() {
const [minute, setMinute] = useState(parseInt(remainingTime/60));
const [second, setSecond] = useState(padZero(remainingTime%60));
const [breaking, setBreaking] = useState(false);
function padZero(num) {
return num.toString().padStart(2,0);
}
function countDown() {
useInterval(() => {
setRemainingTime(remainingTime-1);
}, 1000);
}
React.useEffect(() => {
if (remainingTime === 0) {
setBreaking(!breaking);
setCountDown(false);
setRemainingTime(breakMinute*60)
}
}, [remainingTime]);
React.useEffect(() => {
if (countingDown) {
countDown();
} else {
console.log('Timer stopped!');
}
}, [countingDown]);
return <h1>{minute}:{second}</h1>
};
Using useEffect you can watch the state variable and take action based on that.

Categories