How to clearInterval correctly using React Native - reactjs

In RN, I have a countdown timer using setInterval that goes from 10 - 0.
Once the condition is met of the time === 0 or less than 1, I want the interval to stop.
The countdown is working but is repeating continuously, clearInterval not working.
What am I doing wrong?
import { StyleSheet, Text } from 'react-native'
import React, {useEffect, useState} from 'react'
export default function Timer() {
const [time, setTime] = useState(10)
useEffect(() => {
if(time > 0) {
var intervalID = setInterval(() => {
setTime(time => time > 0 ? time - 1 : time = 10)
}, 1000)
} else {
clearInterval(intervalID)
}
}, [])
return <Text style={styles.timer}>{time}</Text>
}

ClearInterval should be inside, setTime because useEffect will only trigger once.
NOTE: you should also clear on unmount.
export default function Timer() {
const [time, setTime] = useState(10);
useEffect(() => {
var intervalID = setInterval(() => {
setTime((time) => {
if (time > 0) {
return time - 1;
}
clearInterval(intervalID);
return time;
});
}, 1000);
return () => clearInterval(intervalID);
}, []);
return <Text style={styles.timer}>{time}</Text>;
}

The function is on useEffect that only get executed when the component is mounted/unmounted, at the moment where the function is executed, time is 10 so will never go into else condition.
This code must work for you:
import { StyleSheet, Text } from 'react-native'
import React, {useEffect, useState} from 'react'
export default function Timer() {
const [time, setTime] = useState(10)
time <= 0 && clearInterval(intervalID)
useEffect(() => {
var intervalID = setInterval(() => {
setTime(time => time > 0 ? time - 1 : time = 10)
}, 1000)
return () => {
clearInterval(intervalID)
}
}, [])
return <Text style={styles.timer}>{time}</Text>

You can use this also:
import React, { useEffect, useState, useRef } from 'react'
import { Text } from 'react-native'
const Timer = () => {
const [time, setTime] = useState(10)
const promiseRef = useRef(null)
useEffect(() => {
if(time > 0) {
promiseRef.current = setInterval(() => {
setTime(time => time > 0 ? time - 1 : time = 10)
}, 1000)
} else {
promiseRef.current = null
}
}, [])
return <Text style={styles.timer}>{time}</Text>
}
export default Timer

Your useEffect does not have any value(time) inside the dependency array, so it will run only once on component mount.
const [time, setTime] = useState(10);
useEffect(() => {
if (time === 0) {
return;
}
const timeoutId = setInterval(() => {
setTime(time - 1);
}, 1000);
return () => {
clearInterval(timeoutId);
};
}, [time]);
You could also use timeout, where you don't need to clear anything as it will run only once per useEffect trigger.
useEffect(() => {
if (time === 0) {
return;
}
setTimeout(() => {
setTime(time - 1);
}, 1000);
}, [time]);
Another option that might work if you don't want to add time to the dependency array is to clear the interval inside the setTime
useEffect(() => {
const intervalID = setInterval(() => {
setTime((time) => {
if (time === 0) {
clearInterval(intervalID);
return time;
}
return time - 1;
});
}, 1000);
return () => {
clearInterval(intervalID);
};
}, []);
Note that in your example you are counting down to 1 and the going back to 10, so it will never reach 0
setTime(time => time > 0 ? time - 1 : time = 10)
if you want it to reset back to 10 after it finishes count the last option might work for you, as it won't trigger the effect on every time change, just need to return 10 instead of time after clearing the interval
Here is an example https://jsfiddle.net/dnyrm68t/

Related

React useRef() with setInterval() in useCallBack() function not clearing upon clearInterval()

I have this stopwatch code, which shows the elapsed time on screen. (React Component)
It all works okay, but calling "clearInterval(increment.current)" (which using using a useRef() for scope in React) doesn't seem to really "stop" the Interval. It keeps logging "Triggered" in the console (every second) - and I don't follow why it's still being called after clearInterval() when currentActivityOn === false. (And if the timer is triggered several times, it triggers more than once per second.)
Any suggestions?
import React, { useEffect, useRef, useCallback, useContext } from 'react'
import Container from '#material-ui/core/Container'
import Typography from '#material-ui/core/Typography'
import useLocalStorage from '../../hooks/useLocalStorage'
import { FlowTimeContext } from '../../services/flow/flow-time.context'
import { useStyles } from './grid-stopwatch.styles'
export default function GridStopwatch() {
const classes = useStyles()
const { currentActivityOn } = useContext(FlowTimeContext)
const [timerStartTime, setTimerStartTime] = useLocalStorage('timerStartTime', '')
const [timer, setTimer] = useLocalStorage('timer', 0)
const increment = useRef(null)
const handleTimerRun = useCallback(() => {
increment.current = setInterval(() => {
if(timerStartTime !== ''){
const new_timer = ((Math.floor(new Date().getTime() / 1000)) - timerStartTime)
setTimer(new_timer)
} else {
console.log("Triggered") // Why is this being called continually?
}
}, 1000)
}, [setTimer, timerStartTime])
const handleReset = useCallback(() => {
clearInterval(increment.current)
setTimer(0)
setTimerStartTime('')
}, [setTimer, setTimerStartTime])
const handleStart = useCallback(() => {
if(timerStartTime === ''){
var start = Math.floor(new Date().getTime() / 1000)
setTimerStartTime(start)
}
}, [timerStartTime, setTimerStartTime])
const formatTime = () => {
const getSeconds = `0${timer % 60}`.slice(-2)
const minutes = `${Math.floor(timer / 60)}`
const getMinutes = `0${minutes % 60}`.slice(-2)
const getHours = `0${Math.floor(timer / 3600)}`.slice(-2)
return `${getHours}h : ${getMinutes}m : ${getSeconds}s`
}
useEffect(() => {
if(currentActivityOn === true){
handleStart()
handleTimerRun()
} else {
handleReset()
}
}, [currentActivityOn, handleStart, handleReset, handleTimerRun])
return (
<Container maxWidth='xl' className={classes.container}>
{currentActivityOn && (
<Typography variant='subtitle2' gutterBottom>
Current Activity Duration: {formatTime()}
</Typography>
)}
</Container>
)
}
So the useEffect gets triggered twice in this case. So handleTimerRun got called twice, and it seems each time there was an additional interval started that was never cleared.
const handleTimerRun = useCallback(() => {
if(timerStartTime !== ''){
increment.current = setInterval(() => {
const new_timer = ((Math.floor(new Date().getTime() / 1000)) - timerStartTime)
setTimer(new_timer)
}, 1000)
}
}, [setTimer, timerStartTime, increment])
For now I'm able to work around this by moving the condition before starting the interval, so it only gets started once. This seems to solve my issue, but is a workaround rather than truly understanding "how" there can be an additional interval started that is assigned the same id. (and seemingly can't be "stopped")
Further clarity on that would certainly be appreciated.
It seems that if I console log increment.current, it keeps increasing - whereas I thought it would be a consistent ID...? (mutable yes, but consistent when not being mutated.)

useEffect stops working after the first time useState's set becomes stale within a timer

I essentially want a timer to update a displayed calculated time constantly. If I run the setTimeout faster than the expected time the calculation results in a new number for the time (840ms vs 1000ms), the loop ends forever. How do I make this work with React Hooks? Not looking for the answer of just keeping it at 1000ms. I'm trying to find if my usage of useState and useEffect are incorrect or that there's a better hook that I'm not thinking of.
import { useEffect, useState } from "react";
function App() {
const [display, setDisplay] = useState("0");
useEffect(() => {
console.log("display", display);
function calculateTime() {
console.log('timer', Date.now() - 1626712121266);
return Date.now() - 1626712121266;
}
let timeoutId: NodeJS.Timeout;
timeoutId = setTimeout(() => {
console.log("setTimeout")
setDisplay(displayTime(calculateTime()));
}, 840);
return () => {
clearTimeout(timeoutId);
}
}, [display]);
return (
<>
{display}
</>
);
}
function displayTime(milliseconds: number) {
const seconds = Math.floor(milliseconds / 1000 % 60);
const displaySeconds = (seconds < 10) ? '0' + seconds : seconds;
let displayString = "" + displaySeconds;
console.log('displayString', displayString)
return displayString;
}
export default App;
Console results after a refresh. Notice how once seconds stay the same, useEffect stops being called.
How I created this test:
yarn create react-app test-timer --template typescript
Replaced the App() function with what is shown here.
Converting the useState into useReducer with setInterval as the timing function did the trick. useReducer is still like a magical black box to me but it works for now without a hitch. Would love an explanation why this is happening.
import { useEffect, useReducer } from "react";
function App() {
function reducer(state: any, action: any): any {
if (action.type === 'displayUpdate') {
const display = displayTime(Date.now() - 1626712121266);
return {
display: display,
};
}
}
const [state, dispatch] = useReducer(reducer, { display: "0" });
useEffect(() => {
console.log("useEffect");
let intervalId = setInterval(() => {
console.log("setInterval")
dispatch({type: 'displayUpdate'});
}, 500);
return () => {
console.log('useEffect return')
clearInterval(intervalId);
}
}, []);
return (
<>
{state.display}
</>
);
}
function displayTime(milliseconds: number) {
const seconds = Math.floor(milliseconds / 1000 % 60);
const displaySeconds = (seconds < 10) ? '0' + seconds : seconds;
let displayString = "" + displaySeconds;
console.log('displayString', displayString)
return displayString;
}
export default App;

Is there a reason why my clearInterval function isnt working in React?

I've created a simple timer script initialized with 10 seconds for testing. The problem I'm having is that the pause timer button isn't working as expected.
import React, { useState } from 'react';
function App() {
const [time, updateTime] = useState(() => { return 10 });
const [timerRunning, setTimerRunning] = useState(false);
let minutes = Math.floor(time / 60);
let seconds = time % 60;
let interval;
function startTimer() {
setTimerRunning(true);
interval = setInterval( function() {
updateTime(previousTime => previousTime === 0 ? previousTime : previousTime - 1);
}, 1000);
}
function pauseTimer() {
setTimerRunning(false);
clearInterval(interval);
}
function restartTimer() {
setTimerRunning(false);
updateTime(() => {return 10});
}
return (
<>
<p>{minutes > 9 ? minutes : "0" + minutes}:{seconds > 9 ? seconds : "0" + seconds}</p>
<button onClick={startTimer}>Start</button>
<button onClick={pauseTimer}>Pause</button>
<button onClick={restartTimer}>Restart</button>
</>
)
}
export default App;
I want the pause button to pause the timer. Eventually I'll make conditional statements to have each button appear based on the state of the app and the value of time, but the pause button is my current obstacle.
I first had a separate countdown function which used a conditional to stop the time when the time matched counter (below). I thought of a less complicated way that lets me omit the counter variable (above). Im not sure which option is better, or if either is preventing the clearInterval function to work properly. The clearInterval function works within the countdown function if statement, but will not work outside of it.
import React, { useState } from 'react';
function App() {
const [time, updateTime] = useState(() => { return 10 });
const [timerRunning, setTimerRunning] = useState(false);
let counter = 0;
let minutes = Math.floor(time / 60);
let seconds = time % 60;
let interval;
function countdown() {
counter++;
if ( counter === time ) {
setTimerRunning(false);
clearInterval(interval);
}
updateTime(previousTime => previousTime - 1);
}
function startTimer() {
setTimerRunning(true);
interval = setInterval(countdown, 1000);
}
function pauseTimer() {
setTimerRunning(false);
clearInterval(interval);
}
function restartTimer() {
setTimerRunning(false);
updateTime(() => {return 10});
}
return (
<>
<p>{minutes > 9 ? minutes : "0" + minutes}:{seconds > 9 ? seconds : "0" + seconds}</p>
<button onClick={startTimer}>Start</button>
<button onClick={pauseTimer}>Pause</button>
<button onClick={restartTimer}>Restart</button>
</>
)
}
export default App;
Basically you can't create let interval; and assign it a setInterval like interval = setInterval(countdown, 1000);
because on each re-render there will be new let interval;
what you need to do is create a variable which isn't change on re-redners, you can use useRef
const interval = useRef(null);
.....
function startTimer() {
interval.current = setInterval(countdown, 1000);
}
....
function pauseTimer() {
clearInterval(interval.current);
}
and I don't think you need const [timerRunning, setTimerRunning] = useState(false);
find a demo here
Basically when functional component re-renders it will execute from top to bottom, if you use like let counter = 0;, then on each re-render it will initialize to 0, if you need to persists your values in each re-renders you might need some hooks (Ex: useState, useRef ...), in this case useRef would do the trick (because you need only one setInterval in each re-renders and useRef will give you the previous value, it will not re-initalize like a general variable)
You have to use useEffect, like this:
const handleStart = () => {
setChangeValue(true)
}
const handlePause = () => {
setChangeValue(false)
pauseTimer()
}
const handleRestart = () => {
setInitialState()
setChangeValue(true)
}
useEffect(() => {
if (changeValue) {
const interval = setInterval(() => {
startTimer()
}, 100)
return () => clearInterval(interval)
}
}, [changeValue])
you have three buttons to start, pause and restart, invoke these (handleStart, handlePause, handleRestart) functions with them
that is my solution
instead of the startTime function, I use useEffect
useEffect(()=>{
interval = timerRunning && setInterval(() => {
updateTime(previousTime => previousTime === 0 ? previousTime : previousTime - 1);
}, 1000);
return ()=> clearInterval(interval)
},[timerRunning])
and in onClick Start Button
<button onClick={()=> setTimerRunning(true)}>Start</button>
I hope it is useful

counterdown timer in react dont work properly

I am beginner.
I have a question, I have made a counter timer in React, but unfortunately, it doesn't work properly.
I can't find a mistake, Could someone help me?
import React, { useState, useEffect } from "react";
export default function CountDown() {
let [seconds, setSeconds] = useState(3);
let [minutes, setMinutes] = useState(59);
let [hours, setHours] = useState(3);
useEffect(() => {
const interval = setInterval(() => {
setCounddownTimer();
console.log("i am working", { seconds, minutes, hours });
return () => clearInterval(interval);
}, 1000);
}, []);
const setCounddownTimer = () => {
if (hours === 0 && minutes === 0 && seconds === 0) {
timerReset();
} else if (minutes === 0 && seconds === 0) {
console.log({ seconds, minutes, hours });
setHours(--hours);
setMinutes(59);
setSeconds(59);
} else if (seconds === 0) {
setSeconds(59);
setMinutes(--minutes);
} else {
setSeconds(--seconds);
}
};
const timerReset = () => {
setSeconds(59);
setMinutes(59);
setHours(3);
};
const addLeadingZero = (number) => {
return number < 10 ? "0" + number : number;
};
const style = {
"text-align": "center",
"font-weight": "bold",
color: "#cf0000"
};
return (
<div style={style}>
{addLeadingZero(hours)}:{addLeadingZero(minutes)}:
{addLeadingZero(seconds)}
</div>
);
}
I am also paste a link to codesandbox https://codesandbox.io/s/epic-hopper-f9dy4?file=/src/App.js:0-1277
Your useEffect's return statement was inside the setInterval(). It should be outside.
const interval = setInterval(() => {
setCounddownTimer();
console.log("i am working", { seconds, minutes, hours });
}, 1000);
return () => clearInterval(interval);
UPDATE
Also, you should've put hours, minutes, seconds in useEffect's dependency array, since rendering depends on these 3 states. Otherwise the seconds will count down till the timer reaches 03:58:59, but then the minutes will start counting down.

Countdown timer with react hooks

I'm trying to implement countdown timer on my own just to know hooks more. I know there are libraries out there but don't want to use it. the problem with my code is, I cannot get updated state inside "timer" function which is updated in start timer function I'm trying to implement timer that will have triggers to start, stop, & resume & can be manually trigger. by other component that is using the countdown component
import React, { useState } from 'react';
const Countdown = ({ countDownTimerOpt }) => {
const [getObj, setObj] = useState({
formatTimer: null,
countDownTimer: 0,
intervalObj: null,
});
const { formatTimer, countDownTimer, intervalObj } = getObj;
if (countDownTimerOpt > 0 && intervalObj === null) {
startTimer();
}
function startTimer() {
const x = setInterval(() => {
timer();
}, 1000);
setObj((prev) => ({
...prev,
countDownTimer: countDownTimerOpt,
intervalObj: x,
}));
}
function timer() {
var days = Math.floor(countDownTimer / 24 / 60 / 60);
var hoursLeft = Math.floor(countDownTimer - days * 86400);
var hours = Math.floor(hoursLeft / 3600);
var minutesLeft = Math.floor(hoursLeft - hours * 3600);
var minutes = Math.floor(minutesLeft / 60);
var remainingSeconds = countDownTimer % 60;
const formatTimer1 =
pad(days) +
':' +
pad(hours) +
':' +
pad(minutes) +
':' +
pad(remainingSeconds);
if (countDownTimer === 0) {
clearInterval(intervalObj);
} else {
setObj((prev) => ({
...prev,
formatTimer: formatTimer1,
countDownTimer: prev['countDownTimer'] - 1,
}));
}
}
function pad(n) {
return n < 10 ? '0' + n : n;
}
return <div>{formatTimer ? formatTimer : Math.random()}</div>;
};
export default Countdown;
import React, { useState, useEffect } from 'react';
import Timer from '../../components/countdown-timer/countdown.component';
const Training = () => {
const [getValue, setValue] = useState(0);
useEffect(() => {
const x = setTimeout(() => {
console.log('setTimeout');
setValue(10000);
}, 5000);
return () => clearInterval(x);
}, []);
return <Timer countDownTimerOpt={getValue} />;
don't want to use any set interval inside training page as the countdown component will also be used in exam page
Usually with hooks I would combine your functionality into a custom hook and use it in different places.
const useTimer = (startTime) => {
const [time, setTime] = useState(startTime)
const [intervalID, setIntervalID] = useState(null)
const hasTimerEnded = time <= 0
const isTimerRunning = intervalID != null
const update = () => {
setTime(time => time - 1)
}
const startTimer = () => {
if (!hasTimerEnded && !isTimerRunning) {
setIntervalID(setInterval(update, 1000))
}
}
const stopTimer = () => {
clearInterval(intervalID)
setIntervalID(null)
}
// clear interval when the timer ends
useEffect(() => {
if (hasTimerEnded) {
clearInterval(intervalID)
setIntervalID(null)
}
}, [hasTimerEnded])
// clear interval when component unmounts
useEffect(() => () => {
clearInterval(intervalID)
}, [])
return {
time,
startTimer,
stopTimer,
}
}
You can of course add a reset function or do other changes but use could look like this:
const Training = () => {
const { time, startTimer, stopTimer } = useTimer(20)
return <>
<div>{time}</div>
<button onClick={startTimer}>start</button>
<button onClick={stopTimer}>stop</button>
</>
}
You can create a useCountDown Hook as follow (In Typescript) :
Gist
import { useEffect, useRef, useState } from 'react';
export const useCountDown: (
total: number,
ms?: number,
) => [number, () => void, () => void, () => void] = (
total: number,
ms: number = 1000,
) => {
const [counter, setCountDown] = useState(total);
const [startCountDown, setStartCountDown] = useState(false);
// Store the created interval
const intervalId = useRef<number>();
const start: () => void = () => setStartCountDown(true);
const pause: () => void = () => setStartCountDown(false);
const reset: () => void = () => {
clearInterval(intervalId.current);
setStartCountDown(false);
setCountDown(total);
};
useEffect(() => {
intervalId.current = setInterval(() => {
startCountDown && counter > 0 && setCountDown(counter => counter - 1);
}, ms);
// Clear interval when count to zero
if (counter === 0) clearInterval(intervalId.current);
// Clear interval when unmount
return () => clearInterval(intervalId.current);
}, [startCountDown, counter, ms]);
return [counter, start, pause, reset];
};
Usage Demo: https://codesandbox.io/s/usecountdown-hook-56lqv

Resources