How to set "interval id" to state in React - javascript

I'm trying to create an app that allow to create a list of timers. Each timer may be paused and resumed.
const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 1 });
useEffect(() => {
const tick = () => {
const duration = moment.duration(
moment.duration((moment().format('X') - data.unix) * 1000, 'milliseconds') + 1000,
'milliseconds'
);
setTime({ hours: duration.hours(), minutes: duration.minutes(), seconds: duration.seconds() });
};
const timer = setInterval(tick, 1000);
return () => {
clearInterval(timer);
};
}, [data.unix]);
That's all I have for now and I'm trying to make a pause option that I think could be implemented by clearing interval to stop the timer. But timer is in useEffect scope and can't be accessed to delete outside useEffect. If i put timer in setState it causes error. Any advices?

in order to keep reference to the interval outside of useEffect you should use useRef
this will allow clearing the interval on an event handler like: onClick for example.
const IntervalComponent = (props) => {
const intervalRef = useRef();
useEffect(() => {
intervalRef.current = getInterval();
return () => clearInterval(intervalRef.current);
}, []);
const getInterval = () => {
const startTime = new Date().getTime();
const progressInterval = setInterval(() => {
// do on each interval
}, 10);
return progressInterval;
};
const onClickHandler = (e) => clearInterval(intervalRef.current);
return <button onClick={onClickHandler}>Clear interval</button> ;
}

I made this kind of working example (with RN, but the logic stays). I mocked the functionality of moment library: https://snack.expo.io/#zvona/setinterval-example
Like explained in the already upvoted answer, you need to use useRef when working with useEffect and intervals.
Core functionality:
const App = () => {
let timer = useRef();
const [time, setTime] = useState({ hours: 0, minutes: 0, seconds: 1 });
const [toggleLabel, setToggleLabel] = useState('Pause');
const tick = useCallback(() => {
// mock:
const duration = {
hours: () => '0'.padStart(2, '0'),
minutes: () => '0'.padStart(2, '0'),
seconds: () => (''+ Math.floor(Math.random() * 60)).padStart(2, '0'),
};
setTime({
hours: duration.hours(),
minutes: duration.minutes(),
seconds: duration.seconds(),
});
}, []);
const startTicking = () => setInterval(tick, 1e3);
const stopTicking = () => clearInterval(timer.current);
const toggleTimer = () => {
const shouldPause = (toggleLabel === 'Pause');
timer.current = shouldPause ? stopTicking() : startTicking();
setToggleLabel(shouldPause ? 'Resume' : 'Pause');
};
useEffect(() => {
timer.current = startTicking();
return () => {
stopTicking();
};
}, [tick]);
const { hours, minutes, seconds} = time;
return (
<View style={styles.container}>
<Text>{`${hours}:${minutes}:${seconds}`}</Text>
<TouchableOpacity style={styles.toggleButton} onPress={toggleTimer}><Text>{toggleLabel}</Text></TouchableOpacity>
</View>
);
};

Related

React: ClearInterval and Immediately Start Again

I have a component that sets off a timer which updates and makes an axios request every 30 seconds. It uses a useRef which is set to update every 30 seconds as soon as a function handleStart is fired.
const countRef = useRef(null);
const lastUpdatedRef = useRef(null);
const [lastUpdated, setLastUpdated] = useState(Date.now())
const handleStart = () => {
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1);
}, 1000);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
Now I have a useEffect that runs a calculate function every 30 seconds whenever lastUpdated is triggered as a dependency:
const firstCalculate = useRef(true);
useEffect(() => {
if (firstCalculate.current) {
firstCalculate.current = false;
return;
}
console.log("calculating");
calculateModel();
}, [lastUpdated]);
This updates the calculate function every 30 seconds (00:30, 01:00, 01:30 etc.) as per lastUpdatedRef. However, I want the timer to restart from when lastUpdated state has been modified elsewhere (e.g. if lastUpdated was modified at 00:08, the next updated will be 00:38, 01:08, 01:38 etc.). Is there a way to do this?
Basically it sounds like you just need another handler to clear and restart the 30 second interval updating the lastUpdated state.
Example:
const handleOther = () => {
clearInterval(lastUpdatedRef.current);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
}
Full example:
const calculateModel = () => console.log("calculateModel");
export default function App() {
const countRef = React.useRef(null);
const lastUpdatedRef = React.useRef(null);
const [lastUpdated, setLastUpdated] = React.useState(Date.now());
const [timer, setTimer] = React.useState(0);
const handleStart = () => {
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1);
}, 1000);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
const handleOther = () => {
clearInterval(lastUpdatedRef.current);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
const firstCalculate = React.useRef(true);
React.useEffect(() => {
if (firstCalculate.current) {
firstCalculate.current = false;
return;
}
console.log("calculating");
calculateModel();
}, [lastUpdated]);
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<div>Timer: {timer}</div>
<button type="button" onClick={handleStart}>
Start
</button>
<button type="button" onClick={handleOther}>
Other
</button>
</div>
);
}
Don't forget to clear any running intervals when the component unmounts!
React.useEffect(() => {
return () => {
clearInterval(countRef.current);
clearInterval(lastUpdatedRef.current);
};
}, []);

React Hooks Clock - Clear SetInterval After Certain Time

I have a stopwatch function in React that I would like to stop after 15 minutes. I am not sure how to use clearInterval() in this case:
const [timer, setTimer] = useState(0);
const [isActive, setIsActive] = useState(false);
const [isPaused, setIsPaused] = useState(false);
const countRef = useRef(null);
const lastUpdatedRef = useRef(null);
const [minutes,setMinutes] = useState(0)
const [seconds,setSeconds] = useState(0)
const timeCeiling = 900; //maximum minutes is 15
const timeFloor = 60; //maximum seconds is 60 so it resets after
useEffect(() => {
if (timer < timeCeiling) {
setMinutes(Math.floor(timer / 60));
setSeconds(timer % 60);
} else {
setMinutes(15);
setSeconds(0);
}
}, [timer]);
const handleStart = () => {
setIsActive(true);
setIsPaused(true);
countRef.current = setInterval(() => {
setTimer((timer) => timer + 1);
}, 1000);
lastUpdatedRef.current = setInterval(() => {
setLastUpdated(Date.now());
}, 30000);
};
The user clicks on the handleStart function which triggers a useEffect. It also has a lastUpdated dependency which triggers another function every 30 seconds.
The clock should end after 15:00 but it still continues after- where should I put clearInterval so that it stops the clock after 15 minutes? Or is there another way to do this?
I would place it in the useEffect that is running each time timer updates. Clear the interval in the else branch when the limit it hit.
useEffect(() => {
if (timer < timeCeiling) {
setMinutes(Math.floor(timer / 60));
setSeconds(timer % 60);
} else {
clearInterval(countRef.current);
setMinutes(15);
setSeconds(0);
}
}, [timer]);
You might also want to add an additional useEffect hook to clear any running timers should the component unmount before you manually clear them.
useEffect(() => {
return () => {
clearInterval(countRef.current);
clearInterval(lastUpdatedRef.current);
};
}, []);
You can add cleare interval in the else condition:
useEffect(() => {
if (timer < timeCeiling) {
setMinutes(Math.floor(timer / 60));
setSeconds(timer % 60);
} else {
setMinutes(15);
setSeconds(0);
countRef.current && clearInterval(countRef.current);
lastUpdatedRef.current && clearInterval(lastUpdatedRef.current);
}
}, [timer]);
And you should cleare interval when component un-mount:
useEffect(() => {
return () => {
countRef.current && clearInterval(countRef.current);
};
}, [countRef]);
useEffect(() => {
return () => {
lastUpdatedRef.current && clearInterval(lastUpdatedRef.current);
};
}, [lastUpdatedRef]);
I think you should use clearInterval in the else block in useEffect. Maybe this way:
else {
setMinutes(15);
setSeconds(0);
clearInterval(countRef.current) // I hope this works
}

Start interval after time delay and stop on button release / React

I am trying to build a simple plus/minus-control in React. When clicked on either plus or minus (triggered by onMouseDown) the value should change by a defined step and when the button is held the value should in-/decrease at a specified interval after a specified delay. When the button is released (onMouseUp), the interval should stop.
The code below runs ok on onMouseDown and hold, but when I just click on the button the interval starts anyway. I see that I need to make sure that the button is still down before the interval is started, but how do I achieve that? Thank you for any insights.
let plusTimer = useRef(null);
const increment = () => {
setMyValue(prev => prev + myStep);
setTimeout(() => {
plusTimer.current = setInterval(
() => setMyValue(prev => prev + myStep),
100
);
}, 500);
};
const intervalClear = () => {
clearInterval(plusTimer.current);
};
I think I will let the code speak for itself:
const {useCallback, useEffect, useState} = React;
const CASCADE_DELAY_MS = 1000;
const CASCADE_INTERVAL_MS = 100;
function useDelayedCascadeUpdate(intervalTime, delay, step, callback) {
const [started, setStarted] = useState(false);
const [running, setRunning] = useState(false);
const update = useCallback(() => callback((count) => count + step), [
callback,
step
]);
const handler = useCallback(() => {
update();
setStarted(true);
}, [update, setStarted]);
const reset = useCallback(() => {
setStarted(false);
setRunning(false);
}, [setStarted, setRunning]);
useEffect(() => {
if (started) {
const handler = setTimeout(() => setRunning(true), delay);
return () => {
clearTimeout(handler);
};
}
}, [started, setRunning, delay]);
useEffect(() => {
if (running) {
const handler = setInterval(update, intervalTime);
return () => {
clearInterval(handler);
};
}
}, [running, update, intervalTime]);
return [handler, reset];
}
function App() {
const [count, setCount] = useState(0);
const [incrementHandler, incrementReset] = useDelayedCascadeUpdate(
CASCADE_INTERVAL_MS,
CASCADE_DELAY_MS,
1,
setCount
);
const [decrementHandler, decrementReset] = useDelayedCascadeUpdate(
CASCADE_INTERVAL_MS,
CASCADE_DELAY_MS,
-1,
setCount
);
return (
<div>
<div>{count}</div>
<button onMouseDown={incrementHandler} onMouseUp={incrementReset}>
+
</button>
<button onMouseDown={decrementHandler} onMouseUp={decrementReset}>
-
</button>
</div>
);
}
ReactDOM.render(<App />, document.body);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/17.0.1/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/17.0.1/umd/react-dom.production.min.js"></script>

React Native: Timer keeps reseting

I am trying to do a countdown timer but after it gets to 1 it resets to 5 when its supposed to go to '00:00', I don't know where I am going wrong please may someone help me
This is my code:
const CountDown = () => {
const RESET_INTERVAL_S = 5;
const formatTime = (time) =>
`${String(Math.floor(time / 60)).padStart(2, "0")}:${String(
time % 60
).padStart(2, "0")}`;
const Timer = ({ time }) => {
const timeRemain = RESET_INTERVAL_S - (time % RESET_INTERVAL_S);
return (
<>
<Text>{formatTime(timeRemain)}</Text>
</>
);
};
const IntervalTimerFunctional = () => {
const [time, setTime] = useState(0);
console.log("The time is", time);
useEffect(() => {
const timerId = setInterval(() => {
setTime((t) => t + 1);
}, 1000);
return () => clearInterval(timerId);
}, []);
return <Timer time={time} />;
};
return <IntervalTimerFunctional />;
};
I am not sure this is a perfect solution but this could work:
You could stop your timer when it reaches its maximum value:
useEffect(() => {
const timerId = setInterval(() => {
setTime((t) => {
if(t + 1 === RESET_INTERVAL_S) {
clearInterval(timerId)
}
return t + 1;
});
}, 1000);
return () => clearInterval(timerId);
}, []);
And display "00:00" when you have reached the limit:
<Text>{time === RESET_INTERVAL_S ? "00:00" : formatTime(timeRemain)}</Text>
Here is a working example
useEffect(() => {
const timerId = setInterval(() => {
setTime((t) => t + 1);
}, 1000);
return () => clearInterval(timerId);
}, []);
You need to set the value you want to repeat on. If not, this will keep resetting it.
You could also add a condition to check the value of the timer and stop it. like this:
{
time !== 0 ? setTime((t) => t+1): time = 0;
}
Here is a similar problem to yours.

Updated state value is not reflected inside setInterval() in React

I have the following:
const [isPaused, setIsPaused] = useState(false);
const myTimer = useRef(null);
const startTimer = () => {
myTimer.current = setInterval(() => {
console.log(isPaused); // always says "false"
}, 1000);
};
Elsewhere in the code while this timer is running I'm updating the value of isPaused:
setIsPaused(true);
But this isn't reflected in the console log, it always logs false. Is there a fix to this?
The myTimer.current never changed which means isPaused is always false inside the function.
You need to make use of useEffect to update myTimer.current every time isPaused is updated.
useEffect(() => {
function startTimer() {
myTimer.current = setInterval(() => {
console.log(isPaused);
}, 1000);
};
startTimer();
return () => clearInterval(myTimer.current); // cleanup
}, [isPaused]);
You can do something like this,
const [isPaused, setIsPaused] = useState(false);
const myTimer = useRef(null);
const startTimer = () => {
myTimer.current = setInterval(() => {
console.log(isPaused); // now updates
}, 1000);
};
useEffect(() => {
startTimer();
return () => myTimer.current != null && clearInterval(myTimer.current);
}, [isPaused]);
return (
<div>
<b>isPaused: {isPaused ? "T" : "F"}</b>
<button onClick={() => setIsPaused(!isPaused)}>Toggle</button>
</div>
);
Use Others function
use useInterval from 30secondsofcode
const Timer = props => {
const [seconds, setSeconds] = React.useState(0);
useInterval(() => {
setSeconds(seconds + 1);
}, 1000);
return <p>{seconds}</p>;
};
ReactDOM.render(<Timer />, document.getElementById('root'));
Or, use react-useInterval package
function Counter() {
let [count, setCount] = useState(0);
const increaseCount = amount => {
setCount(count + amount);
};
useInterval(increaseCount, 1000, 5);
return <h1>{count}</h1>;
}

Categories