react pomodoro like timer with setInterval - reactjs

The Idea is - I press 'start timer' and a function sets the variable 'sessionSeconds' based on whether its work 25mins or rest 5mins session (true/false). Problem is , when interval restarts, function doesn't update this 'currentSessionSeconds'. Maybe it's something with lifecycles or I should use useEffect somehow..
const TimeTracker = () => {
const [work, setWork] = useState(25);
const [rest, setRest] = useState(5);
const [remainingTime,setRemainingTime]= useState(25);
const [iswork, setIswork] = useState(true);
function startTimer() {
clearInterval(interval);
let sessionSeconds = iswork? work * 60 : rest * 60
interval = setInterval(() => {
sessionSeconds--
if (duration <= 0) {
setIswork((iswork) => !iswork)
updateTime(sessionSeconds)
startTimer();
}
}, 1000);
function updateTime(seconds){
setRemainingTime(seconds)
}
}
return (
<div>
<p>
{remainingTime}
</p>
<button onClick={startTimer}>timer</button>
</div>
);
}
I didnt include other code for converting, etc to not over clutter.

I couldn't get your code working to test it since it is missing a }.
I changed your code to include use effect and everything seems to work fine.
https://codesandbox.io/s/wizardly-saha-0dxde?file=/src/App.js
const TimeTracker = () => {
/* In your code you used use state however you didn't change
the state of these variables so I set them to constants.
You can also pass them through props.
*/
const work = 25;
const rest = 5;
const [remainingTime, setRemainingTime] = useState(work * 60);
const [isWork, setIsWork] = useState(true);
const [isTimerActive, setIsTimerActive] = useState(false);
const startTimerHandler = () => {
isWork ? setRemainingTime(work * 60) : setRemainingTime(rest * 60);
setIsTimerActive(true);
};
useEffect(() => {
if (!isTimerActive) return;
if (remainingTime === 0) {
setIsWork((prevState) => !prevState);
setIsTimerActive(false);
}
const timeOut = setTimeout(() => {
setRemainingTime((prevState) => prevState - 1);
}, 1000);
/* The return function will be called before each useEffect
after the first one and will clear previous timeout
*/
return () => {
clearTimeout(timeOut);
};
}, [remainingTime, isTimerActive]);
return (
<div>
<p>{remainingTime}</p>
<button onClick={startTimerHandler}>timer</button>
</div>
);
};

Related

setInterval pause inside useEffect

UPDATE: OMG I'm using setTimeOut. But I still need an answer.
I made an application that accesses an array and outputs each of its elements after a certain period of time. When the array ends, execution stops.
There is a need to pause the execution. How can I do that?
const [isPaused, setIsPaused] = useState(false);
const togglePause = () => {
setIsPaused(!isPaused)
}
const soccData = data.player_positions; // cutting only IDs and positions from data
const [playerPosition, setPlayerPosition] = useState([]); // creating local state to work with IDs and positions
const getPlayerData = (arr) => { // function goes thru array of players and sets a new playerPosition on every step
for (let i = 0; i < arr.length; i++) {
setTimeout(() => {
setPlayerPosition(arr[i])
}, data.interval * (i + 1));
}
}
useEffect(() => {
getPlayerData(soccData)
return () => {
clearTimeout()
};
}, [soccData]);
return (
<div>{playerPosition}</div>
<p><button onClick={togglePause}></button></p>
)
I tried adding a condition if(!isPaused) to the function getPlayerData (and a dependency to useSeffect), but that didn't work.
Here's my code on codesandbox: https://codesandbox.io/s/youthful-paper-pvrq09
p.s. I found someone's code that allows to start/pause the execution: https://jsfiddle.net/thesyncoder/12q8r3ex/1/, but there's no ability to stop the execution when array ends.
The following works in your sandbox:
export default function App() {
const [isPaused, setIsPaused] = useState(false);
const [frame, setFrame] = useState(0);
const togglePause = () => {
setIsPaused(!isPaused);
};
const playerPosition = data.player_positions[frame];
useEffect(() => {
// do nothing if paused or the last frame is reached
if (isPaused || frame === data.player_positions.length - 1) {
return;
}
const to = setTimeout(() => {
setFrame((f) => f + 1);
}, data.interval);
return () => {
clearTimeout(to);
};
}, [frame, isPaused]);
return (
<div className="App">
<pre>{playerPosition}</pre>
<p>
<button onClick={togglePause}>{isPaused ? "play" : "pause"}</button>
</p>
</div>
);
}
As you can see, I removed the getPlayerData function. Instead, when the frame (which I called the index of the data we're currently at) or the isPaused variable changes, the effect is reran. It first (except for the very first execution) cleans up the still pending timeout (if there is one) and schedules a new one (if not paused and end is not reached) that increments the frame by one.
The main problem with your solution is the following:
const getPlayerData = (arr) => { // function goes thru array of players and sets a new playerPosition on every step
for (let i = 0; i < arr.length; i++) {
setTimeout(() => {
setPlayerPosition(arr[i])
}, data.interval * (i + 1));
}
}
This does not wait for the first timeout to occur, but instead schedules all frames in advance.
Therefore it is not pausable in a practical way (you could keep track of which timeout already ran, clear all of them when pausing and scheduling only the pending ones again once unpaused but that is not really practical)
setInterval() returns its id on execution. You can pass that id to clearInterval() to cancel it. Something like this might work:
let interval = 0;
interval = setInterval(() => {
// Your program logic
// ...
if(conditionTrueToCancelInterval) {
clearInterval(interval);
}
}, 1000)
import { ChangeEvent, useState } from 'react'
import { useInterval } from 'usehooks-ts'
export default function Component() {
// The counter
const [count, setCount] = useState<number>(0)
// Dynamic delay
const [delay, setDelay] = useState<number>(1000)
// ON/OFF
const [isPlaying, setPlaying] = useState<boolean>(false)
useInterval(
() => {
// Your custom logic here
setCount(count + 1)
},
// Delay in milliseconds or null to stop it
isPlaying ? delay : null,
)
const handleChange = (event: ChangeEvent<HTMLInputElement>) => {
setDelay(Number(event.target.value))
}
return (
<>
<h1>{count}</h1>
<button onClick={() => setPlaying(!isPlaying)}>
{isPlaying ? 'pause' : 'play'}
</button>
<p>
<label htmlFor="delay">Delay: </label>
<input
type="number"
name="delay"
onChange={handleChange}
value={delay}
/>
</p>
</>
)
}

How to start and stop timer display in ReactJS

I am trying to create a Pomodoro timer in ReactJS. I am having trouble having the timer to stop it's countdown.
PomView.js
const PomView = () => {
const [timer, setTimer] = useState(1500) // 25 minutes
const [start, setStart] = useState(false)
var firstStart = useRef(true)
var tick;
useEffect( () => {
if (firstStart.current) {
console.log("first render, don't run useEffect for timer")
firstStart.current = !firstStart.current
return
}
console.log("subsequent renders")
console.log(start)
if (start) {
tick = setInterval(() => {
setTimer(timer => {
timer = timer - 1
console.log(timer)
return timer
}
)
}, 1000)
} else {
console.log("clear interval")
clearInterval(tick);
}
}, [start])
const toggleStart = () => {
setStart(!start)
}
const dispSecondsAsMins = (seconds) => {
// 25:00
console.log("seconds " + seconds)
const mins = Math.floor(seconds / 60)
const seconds_ = seconds % 60
return mins.toString() + ":" + ((seconds_ == 0) ? "00" : seconds_.toString())
}
return (
<div className="pomView">
<ul>
<button className="pomBut">Pomodoro</button>
<button className="pomBut">Short Break</button>
<button className="pomBut">Long Break</button>
</ul>
<h1>{dispSecondsAsMins(timer)}</h1>
<div className="startDiv">
{/* event handler onClick is function not function call */}
<button className="startBut" onClick={toggleStart}>{!start ? "START" : "STOP"}</button>
{start && <AiFillFastForward className="ff" onClick="" />}
</div>
</div>
)
}
export default PomView
Although the clearInterval runs in the else portion of useEffect, the timer continues ticking. I am not sure if it is because of the asynchronous setTimer method in useEffect. I would like to know what the problem is with the code I have written.
You store the timer ref in tick, but each time the component rerenders the tick value from the previous render is lost. You should also store tick as a React ref.
You are also mutating the timer state.
setTimer((timer) => {
timer = timer - 1; // mutation
return timer;
});
Just return the current value minus 1: setTimer((timer) => timer - 1);
Code
const PomView = () => {
const [timer, setTimer] = useState(1500); // 25 minutes
const [start, setStart] = useState(false);
const firstStart = useRef(true);
const tick = useRef(); // <-- React ref
useEffect(() => {
if (firstStart.current) {
firstStart.current = !firstStart.current;
return;
}
if (start) {
tick.current = setInterval(() => { // <-- set tick ref current value
setTimer((timer) => timer - 1);
}, 1000);
} else {
clearInterval(tick.current); // <-- access tick ref current value
}
return () => clearInterval(tick.current); // <-- clear on unmount!
}, [start]);
...
};
useEffect( () => {
const tick= setInterval(fun, 1000);
return ()=>{
clearInterval(tick);
}
}, [])
useEffect has it's own release way.

useEffect with setTimeout/setInterval to dismiss alerts

I'm trying to get each alert dismissed after some time passes but when I try calling setTimeout/Interval in useEffect, my alerts dont display or dismiss properly. As it stands now, theres always one alert that doesnt get dismissed because allAlerts isnt consistently updated. How can I add setTimeout or setInterval to run my checkAlertTimes function after useEffect has stopped updating?
const [filteredAlerts, setFilteredAlerts] = useState()
const checkAlertTimes = () => {
const currentTime = new Date().getTime()
setFilteredAlerts(alertsToday.filter(alert => {
const alertTime = alert.date.getTime()
const secondsPassed = (currentTime - alertTime) /1000
if (secondsPassed < 30){return alert}
}))
}
useEffect(() => {
checkAlertTimes()
},[allAlerts])
return (
<div>{//map through and show alerts}</div>
)
From where I see you need something like this:
const [filteredAlerts, setFilteredAlerts] = useState();
const checkAlertTimes = () => {
const currentTime = new Date().getTime();
setFilteredAlerts(
alertsToday.filter((alert) => {
const alertTime = alert.date.getTime();
const secondsPassed = (currentTime - alertTime) / 1000;
if (secondsPassed < 30) {
return alert;
}
})
);
};
useEffect(() => {
const interval = setInterval(() => {
checkAlertTimes();
}, 1000);
return () => clearInterval(interval);
}, [allAlerts, alertsToday]);
return (
<div>
{
//map through and show alerts
}
</div>
);

How do I remove multiple setIntervals from useEffect

I'm loading multiple animals into my ThreeJS project. All these animals have PositionalAudio with a setInterval function. I use them inside a useEffect function. On the callback I want to clear the interval, but it keeps calling the function.
This is the function where I set my setInterval:
const loadAudio = () => {
const animalSound = new THREE.PositionalAudio(listener);
animalSound.setBuffer(animalBuffer);
playSounds = setInterval(() => {
animalSound.play();
} , 5000);
audios.push(animalSound);
}
In the return function I try to clear the interval:
return () => {
audios.forEach((audio) => {
audio.stop();
clearInterval(playSounds);
});
};
Sadly the audio keeps playing every 5 seconds
Here is a code snippet
https://codesandbox.io/s/bitter-tree-bb4ld?file=/src/App.js
According to your code snippet, say you have Button:
<button
onClick={buttonToggle}
>
{start ? 'start' : 'stop'}
</button>
Initially we have some setup for useState and handle click function
const [seconds, setSeconds] = useState(0);
const [btnStart, setBtnStart] = useState(true);
const buttonToggle = useCallback(
() => setBtnStart(run => !run)
, []);
In the useEffect you will do following changes
useEffect(() => {
if(!btnStart) {
// setSeconds(0); // if you want to reset it as well
return;
}
const interval = setInterval(() => {
setSeconds(seconds => seconds + 1);
}, 1000);
return () => clearInterval(interval);
}, [btnStart]);

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