I'm quite new to ReactJS and have been working on a Pomodoro Timer. How it works is that whenever the "Work Timer" reaches zero, it switches to the "Break Timer".
const [secondsLeft, setSecondsLeft] = useState(newTimer.work);
However,
this line: setSecondsLeft(nextSeconds); // This does not update secondsLeft
under useEffect, does not update the secondsLeft which results in the timer for the break to display wrongly.
Could use some advice to make this work.
Thanks!
import {
Button,
Heading,
VStack,
Stack,
HStack,
Box,
CircularProgress,
Text,
CircularProgressLabel,
} from '#chakra-ui/react';
import { useState, useRef, useContext, useEffect } from 'react';
import { SettingsContext } from '../helpers/SettingsContext';
import PomodoroSettings from './PomodoroSettings';
// Pomodoro
const Pomodoro = () => {
//Timer
const { newTimer, setNewTimer } = useContext(SettingsContext);
const [isPaused, setIsPaused] = useState(true);
const [secondsLeft, setSecondsLeft] = useState(newTimer.work);
const secondsLeftRef = useRef(secondsLeft);
const isPausedRef = useRef(isPaused);
const modeRef = useRef(newTimer.active);
function tick() {
secondsLeftRef.current--;
setSecondsLeft(secondsLeftRef.current);
}
useEffect(() => {
function switchMode() {
const nextMode = modeRef.current === 'work' ? 'break' : 'work';
const nextSeconds =
(nextMode === 'work' ? newTimer.work : newTimer.short) * 60;
setNewTimer({
work: newTimer.work,
short: newTimer.short,
long: newTimer.long,
active: nextMode,
});
modeRef.current = nextMode;
console.log(nextMode);
console.log('Next: ' + nextSeconds);
setSecondsLeft(nextSeconds); // This does not update secondsLeft
console.log('SecondsLeft: ' + secondsLeft);
secondsLeftRef.current = nextSeconds;
}
secondsLeftRef.current = newTimer.work * 60;
setSecondsLeft(secondsLeftRef.current);
const interval = setInterval(() => {
if (isPausedRef.current) {
return;
}
if (secondsLeftRef.current === 0) {
return switchMode();
}
tick();
}, 1000);
return () => clearInterval(interval);
}, [newTimer, setNewTimer]);
const totalSeconds =
newTimer.active === 'work' ? newTimer.work * 60 : newTimer.short * 60;
const percentage = Math.round((secondsLeft / totalSeconds) * 100);
const minutes = Math.floor(secondsLeft / 60);
let seconds = secondsLeft % 60;
if (seconds < 10) seconds = '0' + seconds;
// Start / Pause
const handleButton = (e) => {
e.preventDefault();
if (isPaused) {
setIsPaused(!isPaused);
isPausedRef.current = false;
} else {
setIsPaused(!isPaused);
isPausedRef.current = true;
}
};
// console.log(percentage)
// console.log(newTimer.active)
// console.log(secondsLeft)
// console.log(totalSeconds)
return (
<div>
<Heading as='h4' size='md'>
{' '}
Pomodoro{' '}
</Heading>
<VStack>
<HStack>
<Button variant='ghost'>Pomodoro</Button>
<Button variant='ghost'>Short Break</Button>
<Button variant='ghost'>Long Break</Button>
</HStack>
<CircularProgress
value={percentage}
color={newTimer.active === 'work' ? 'red.400' : 'green.400'}
size='200px'
thickness='10px'
>
<CircularProgressLabel>
<Text fontSize='3xl'>{minutes + ':' + seconds}</Text>
</CircularProgressLabel>
</CircularProgress>
<HStack>
<Button variant='outline' onClick={handleButton}>
{isPaused ? <div> START </div> : <div> STOP </div>}
</Button>
<PomodoroSettings />
</HStack>
</VStack>
</div>
);
};
export default Pomodoro;
useState uses array const [value, setValue] like this. It might be just an error in this declaration of newTimer.
There is too much going on in one eseEffect(). The initial value of newTimer.work is not modified so each re-render will set secondsLeftRef to the same initial value.
Since useEffect()updates a context object, the component will re-render on every iteration, setting secondsLeftRef to its initial value.
Not sure where the problem is but few things I noticed that you can try to fix or improve in your code -
Do not use or assign context or props value in useState as you are doing it for secondsLeft, reason is that useState is supposed to run once when component gets rendered first time and you might not get value for newTimer.work on very first render.
For this you can use useEffect and newTimer in dependency array.
const [secondsLeft, setSecondsLeft] = useState(newTimer.work);
You are trying to log secondsLeft immediatly after calling setSecondsLeft this will not give you correct value because setting state is asynchronous in react so it will not be available on next line after calling set state.
setSecondsLeft(nextSeconds);
console.log('SecondsLeft: ' + secondsLeft);
One last thing I noticed looking at your code is that it's possible that you might be registering multiple setInterval because below line is responsible for clearing interval only when component gets unmount but your useEffect will get called multiple times based on its dependencies.
return () => clearInterval(interval);
Try to debug your code for above 3rd point and if that's the problem you
can try clearing your interval in start of useEffect or add
some conditions to make sure it will get register once to get
expected results.
Note - I have not tried executing your code, but let me know if this helps or if I can improve my answer in any way.
Related
Being new to react , this all is really confusing and new to me , so I apologise if I'm making some obvious oversight.
Im making a stopwatch and implementing the seconds for starters. However; Im confused as to how i'll implement the on display seconds number to update when each second passes.
This is what I'm doing right now
function App() {
const [time , updateTime] = React.useState(0);
var startsec = 0;
//UpdateTime should get triggered when next second passes
const UpdateTime = () => {
//Update time variable with the new seconds elapsed
}
//Should run every second or something
const CheckTimeUpdation = () => {
currentsec = Math.floor(Date.now() / 1000.0);
console.log(currentsec);
if(currentsec > startsec){
UpdateTime(currentsec-startsec);
}
}
const GetStartTime = () => {
startsec = Math.floor(Date.now() / 1000.0);
}
//Clock component just gets a number and displays it on the screen
return (<div className = "App">
<Clock timerSeconds= {time}/>
<div>
<button onClick={GetStartTime}></button>
</div>
</div>);
}
export default App;
Date.now() function gets the miliseconds passed since 1970 (hence the division by 1000 to make them into seconds) and I find the difference between when the button was clicked and current one and passs that to the time component to display.
How do I make the CheckTimeUpdation function run every second or so?
What you want is the setInterval() method (see: https://developer.mozilla.org/en-US/docs/Web/API/setInterval)
However your code so far has some issues:
On the button click, getStartTime runs and it updates the value of startsec. Firstly, this does not cause the component to re-render, and so the component will not update and you will see nothing changing on your screen. Also, if you did get your component to re-render, you will notice that startsec will be 0 again on the next re-render, so re-assigning startsec like how you did likely doesn't do what you want it to. If you want to persist values between rerenders, you can use useState (https://reactjs.org/docs/hooks-reference.html#usestate) or useRef (https://reactjs.org/docs/hooks-reference.html#useref).
Now i'm assuming you want to start the timer when the button is clicked. What you need is to start the interval (via setInterval) on the button click, and update time every 1000ms.
You'd also want to clear the interval once you don't need it anymore, using the clearInterval() method. You'll need to save the id returned from setInterval() initially in order to clear it later.
Below I have written a short example using this idea with setInterval on button click to help you:
import { useState, useRef } from "react";
export default function App() {
const [timerState, setTimerState] = useState("paused");
const [timeElapsed, setTimeElapsed] = useState(0);
const intervalId = useRef(0);
const isRunning = timerState === "running";
const isPaused = timerState === "paused";
const onStartTimer = () => {
intervalId.current = setInterval(() => {
setTimeElapsed((time) => time + 1);
}, 1000);
setTimerState("running");
};
const onStopTimer = () => {
clearInterval(intervalId.current);
intervalId.current = 0;
setTimerState("paused");
};
const onClearTimer = () => {
setTimeElapsed(0);
};
return (
<div className="App">
<h1>{timeElapsed}</h1>
{isPaused && <button onClick={onStartTimer}>Start timer</button>}
{isRunning && <button onClick={onStopTimer}>Stop timer</button>}
{!!timeElapsed && <button onClick={onClearTimer}>Clear timer</button>}
</div>
);
}
You can use setInterval in the GetStartTime Function.
const GetStartTime = () => {
startsec = Math.floor(Date.now() / 1000.0);
setInterval(() => CheckTimeUpdation(), 1000);
}
I am trying to set up an image carousel that loops through 3 images when you mouseover a div. I'm having trouble trying to figure out how to reset the loop after it reaches the third image. I need to reset the setInterval so it starts again and continuously loops through the images when you are hovering over the div. Then when you mouseout of the div, the loop needs to stop and reset to the initial state of 0. Here is the Code Sandbox:
https://codesandbox.io/s/pedantic-lake-wn3s7
import React, { useState, useEffect } from "react";
import { images } from "./Data";
import "./styles.css";
export default function App() {
let timer;
const [count, setCount] = useState(0);
const updateCount = () => {
timer = setInterval(() => {
setCount((prevCount) => prevCount + 1);
}, 1000);
if (count === 3) clearInterval(timer);
};
const origCount = () => {
clearInterval(timer);
setCount((count) => 0);
};
return (
<div className="App">
<div className="title">Image Rotate</div>
<div onMouseOver={updateCount} onMouseOut={origCount}>
<img src={images[count].source} alt={images.name} />
<p>count is: {count}</p>
</div>
</div>
);
}
Anything involving timers/intervals is an excellent candidate for useEffect, because we can easily register a clear action in the same place that we set the timer using effects with cleanup. This avoids the common pitfalls of forgetting to clear an interval, e.g. when the component unmounts, or losing track of interval handles. Try something like the following:
import React, { useState, useEffect } from "react";
import { images } from "./Data";
import "./styles.css";
export default function App() {
const [count, setCount] = useState(0);
const [mousedOver, setMousedOver] = useState(false);
useEffect(() => {
// set an interval timer if we are currently moused over
if (mousedOver) {
const timer = setInterval(() => {
// cycle prevCount using mod instead of checking for hard-coded length
setCount((prevCount) => (prevCount + 1) % images.length);
}, 1000);
// automatically clear timer the next time this effect is fired or
// the component is unmounted
return () => clearInterval(timer);
} else {
// otherwise (not moused over), reset the counter
setCount(0);
}
// the dependency on mousedOver means that this effect is fired
// every time mousedOver changes
}, [mousedOver]);
return (
<div className="App">
<div className="title">Image Rotate</div>
<div
// just set mousedOver here instead of calling update/origCount
onMouseOver={() => setMousedOver(true)}
onMouseOut={() => setMousedOver(false)}
>
<img src={images[count].source} alt={images.name} />
<p>count is: {count}</p>
</div>
</div>
);
}
As to why your code didn't work, a few things:
You meant to say if (count === 2) ..., not count === 3. Even better would be to use the length of the images array instead of hardcoding it
Moreover, the value of count was stale inside of the closure, i.e. after you updated it using setCount, the old value of count was still captured inside of updateCount. This is actually the reason to use functional state updates, which you did when you said e.g. setCount((prevCount) => prevCount + 1)
You would have needed to loop the count inside the interval, not clear the interval on mouse over. If you think through the logic of it carefully, this should hopefully be obvious
In general in react, using a function local variable like timer is not going to do what you expect. Always use state and effects, and in rarer cases (not this one), some of the other hooks like refs
I believe that setInterval does not work well with function components. Since callback accesses variables through closure, it's really easy to shoot own foot and either get timer callback referring to stale values or even have multiple intervals running concurrently. Not telling you cannot overcome that, but using setTimeout is much much much easier to use
useEffect(() => {
if(state === 3) return;
const timerId = setTimeout(() => setState(old => old + 1), 5000);
return () => clearTimeout(timerId);
}, [state]);
Maybe in this particular case cleanup(clearTimeout) is not required, but for example if user is able to switch images manually, we'd like to delay next auto-change.
The timer reference is reset each render cycle, store it in a React ref so it persists.
The initial count state is closed over in interval callback scope.
There are only 3 images so the last slide will be index 2, not 3. You should compare against the length of the array instead of hard coding it.
You can just compute the image index by taking the modulus of count state by the array length.
Code:
export default function App() {
const timerRef = useRef();
const [count, setCount] = useState(0);
// clear any running intervals when unmounting
useEffect(() => () => clearInterval(timerRef.current), []);
const updateCount = () => {
timerRef.current = setInterval(() => {
setCount((count) => count + 1);
}, 1000);
};
const origCount = () => {
clearInterval(timerRef.current);
setCount(0);
};
return (
<div className="App">
<div className="title">Image Rotate</div>
<div onMouseOver={updateCount} onMouseOut={origCount}>
<img
src={images[count % images.length].source} // <-- computed index to cycle
alt={images.name}
/>
<p>count is: {count}</p>
</div>
</div>
);
}
Your setCount should use a condition to check to see if it should go back to the start:
setCount((prevCount) => prevCount === images.length - 1 ? 0 : prevCount + 1);
This will do setCount(0) if we're on the last image—otherwise, it will do setCount(prevCount + 1).
A faster (and potentially more readable) way of doing this would be:
setCount((prevCount) => (prevCount + 1) % images.length);
It keeps showing the error message that it is an infinite loop. I am only beggining to learn React, and this is a Clicker game. How do I change my code to make the setInterval work. Thank you.(BTW I do not want any other changes to the code that won't affect the setInterval function. And yes, I have used setInterval in many projects already and it worked out fine.)
import "./styles.css";
export default function App() {
let [num, setNum] = useState(0);
let [add, setAdd] = useState(1);
let [numC, setNumC] = useState(0);
let [numCP, setNumCP] = useState(10);
let [numW, setNumW] = useState(0);
let [numWP, setNumWP] = useState(20)
setInterval(setNum(num+=numW),3000);
const click = () => {
setNum((num += add));
};
const clicker = () => {
if (num >= numCP) {
setNumC((numC += 1));
setNum((num -= numCP));
setNumCP((numCP += 5));
setAdd((add += 1));
}
};
const worker = () => {
if (num >= numWP) {
setNumW((numW += 1));
setNum((num -= numWP));
setNumWP((numWP += 10));
}
};
return (
<div className="App">
<h1>Clicker Game</h1>
<div>
{num}
<button onClick={click}>Click</button>
</div>
<p />
<div>
{numC}
<button onClick={clicker}>Buy({numCP})</button>
</div>
<div>
{numW}
<button onClick={worker}>Buy({numWP})</button>
</div>
</div>
);
}```
There are a couple of issues.
First you are immediately calling the setNum when you should be passing a callback to be executed when the interval is passed.
So setInterval(() => setNum(num+=numW),3000);
But now you have the second issue, each time the component is re-rendered you will initiate an additional interval. (and it will be re-rendered a lot, at the minimum each time the interval callback is fired)
So you would likely need to use a useEffect, with 0 dependencies so it runs once, if you want to set it and let it run continuously.
useEffect(() => {
setInterval(() => setNum(num += numW), 3000);
}, []);
But now you will encounter yet another issue. The num and numW used in the interval will be locked to the values in the first render of the component.
For the num you can work around it, by using the callback syntax of the setNum
useEffect(() => {
setInterval(() => setNum(currentNum => currentNum += numW), 3000);
}, []);
but numW will never update.
A final tool, is to reset the interval each time the numW or num changes. To do that you will need to return a function from the useEffect that does the clearing.
useEffect(() => {
const interval = setInterval(() => setNum(currentNum => currentNum += numW), 3000);
return () => clearInterval(interval);
}, [numW]);
But this will have the minor issue that the interval is now not constant, since it resets.
Every time one of your state variables changes, the component is re-rendered, i.e. the function App is called again. This will call setInterval over and over again.
You want to look at useEffect to put your setIntervals in.
i have a timer which shows the timer based on the time left by comparing it from now to the time received from server and shows timer what i have done is below, received time is the string like "2020-09-02T05:09:56.119Z" now i want the time to be the difference between time received from server and the time now but my timer is showing two times only as shown in the below gif
Link to the problem timer
import React, {useState, useEffect} from 'react';
import {Box, Message, Video, Timer, BlueScreen, Emoji, Heading, SubHeading, WaitingImage} from './styled' ;
import Button from 'Components/Button';
import { getAPatient } from '../api';
import moment from 'moment';
export default function WaitingRoom(){
const { t } = useTranslation();
const [timeLeft, setTimeLeft] = useState();
const url = window.location.href.split('?id=');
const id = url[1];
const geTimerTime = async () => {
await getAPatient(id)
.then((info) => {
const datetime = info && info.data.datetime;
const currentTime = moment().toISOString()
const d1 = new Date(currentTime);
const d2 = new Date(datetime);
const difference = d1 - d2;
if (difference > 60e3){
const minutes = Math.floor(difference / 60e3);
const seconds = minutes * 60;
setTimeLeft(seconds)
}
else {
const seconds = Math.floor(difference / 1e3);
setTimeLeft(seconds)
}
console.log(currentTime, datetime,difference,"infopoooo")
})
.catch((err) => {
console.log(err)
});
}
useEffect(() => {
geTimerTime()
if (!timeLeft) return;
}, [timeLeft]);
return(
timeLeft === 0 ?
<BlueScreen>
<Emoji>
<Smiley />
</Emoji>
<Heading>
{t('turnMessageHeading')}
</Heading>
<SubHeading>
{t('turnMessageSubHeading')}
</SubHeading>
<SubHeading>
{t('turnMessageSubHeadingDoctor')}
</SubHeading>
<Button
themeWhite
>
{t('commingMessage')}
</Button>
</BlueScreen>
:
<Box>
<Button
themeBlue
width={'100%'}
textAlign={'left'}
>
{t('Virtual_waiting_room')}
</Button>
<Message>
{t('waiting_message')}
</Message>
<Timer>
<div>{Math.floor(timeLeft/60) + ':' + ('0' + Math.floor(timeLeft % 60)).slice(-2)}</div> minutes
</Timer>
<WaitingImage>
<Waiting />
</WaitingImage>
<Video>
<iframe src='https://www.youtube.com/embed/gaka1vqYFNs'
frameborder='0'
allow='autoplay; encrypted-media'
allowfullscreen
title='video'
width={"100%"}
/>
</Video>
</Box>
)
}
It is simply because you call setTimeLeft, which sets the state for timeLeft, which in react triggers a re-render, during which all the useEffect() hooks will run, except for those that are not listening to changes on a state variable.
Furthermore, you have timeLeft in the dependency array (the second parameter in the hook), which means this useEffect() will run on every re-render where timeLeft state was set, thus running on each iteration.
If you need your useEffect() to run like componentDidMount, you need to keep an empty dependency array, thus ensuring the hook only runs on initial render
useEffect(() => {
geTimerTime()
if (!timeLeft) return;
}, []);//empty dependency array to mimic componentDidMount
useEffect's set state Function should not match the second parameter. The useEffects is run whenever any data inside the second parameter's array is changed. Since you are changing the timeLeft inside the useEffects it will run infinitely. If you want to do geTimerTime anyway and have the timeLeft inside the dependency array(second parameter to useEffect) then you can do like below
useEffect(() => {
if (timeLeft) {
geTimerTime();
}
}, [timeLeft])
I have the following component defined in my app scaffolded using create-react:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
setTimer();
return (
<div>
<div>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
And currentSecond is updated every second until it hits the props.secondsPerRep however if I try to start the setInterval from a click handler:
import React, { useState } from 'react';
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
let timer;
const setTimer = () => {
timer = setInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(() => currentSecond + 1);
}
}, 1000);
}
return (
<div>
<div>
<button onClick={setTimer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Then currentSecond within the setInterval callback always returns to the initial value, i.e. 1.
Any help greeeeeeatly appreciated!
Your problem is this line setCurrentSecond(() => currentSecond + 1); because you are only calling setTimer once, your interval will always be closed over the initial state where currentSecond is 1.
Luckily, you can easily remedy this by accessing the actual current state via the args in the function you pass to setCurrentSecond like setCurrentSecond(actualCurrentSecond => actualCurrentSecond + 1)
Also, you want to be very careful arbitrarily defining intervals in the body of functional components like that because they won't be cleared properly, like if you were to click the button again, it would start another interval and not clear up the previous one.
I'd recommend checking out this blog post because it would answer any questions you have about intervals + hooks: https://overreacted.io/making-setinterval-declarative-with-react-hooks/
https://overreacted.io/making-setinterval-declarative-with-react-hooks/ is a great post to look at and learn more about what's going on. The React useState hook doesn't play nice with setInterval because it only gets the value of the hook in the first render, then keeps reusing that value rather than the updated value from future renders.
In that post, Dan Abramov gives an example custom hook to make intervals work in React that you could use. That would make your code look more like this. Note that we have to change how we trigger the timer to start with another state variable.
const Play = props => {
const [currentSecond, setCurrentSecond] = React.useState(1);
const [isRunning, setIsRunning] = React.useState(false);
useInterval(() => {
if (currentSecond < props.secondsPerRep) {
setCurrentSecond(currentSecond + 1);
}
}, isRunning ? 1000 : null);
return (
<div>
<div>
<button onClick={() => setIsRunning(true)}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
I went ahead and put an example codepen together for your use case if you want to play around with it and see how it works.
https://codepen.io/BastionTheDev/pen/XWbvboX
That is because you're code is closing over the currentSecond value from the render before you clicked on the button. That is javascript does not know about re-renders and hooks. You do want to set this up slightly differently.
import React, { useState, useRef, useEffect } from 'react';
const Play = ({ secondsPerRep }) => {
const secondsPassed = useRef(1)
const [currentSecond, setCurrentSecond] = useState(1);
const [timerStarted, setTimerStarted] = useState(false)
useEffect(() => {
let timer;
if(timerStarted) {
timer = setInterval(() => {
if (secondsPassed.current < secondsPerRep) {
secondsPassed.current =+ 1
setCurrentSecond(secondsPassed.current)
}
}, 1000);
}
return () => void clearInterval(timer)
}, [timerStarted])
return (
<div>
<div>
<button onClick={() => setTimerStarted(!timerStarted)}>
{timerStarted ? Stop : Start}
</button>
<p>{currentSecond}</p>
</div>
</div>
);
}
export default Play;
Why do you need a ref and the state? If you would only have the state the cleanup method of the effect would run every time you update your state. Therefore, you don't want your state to influence your effect. You can achieve this by using the ref to count the seconds. Changes to the ref won't run the effect or clean it up.
However, you also need the state because you want your component to re-render once your condition is met. But since the updater methods for the state (i.e. setCurrentSecond) are constant they also don't influence the effect.
Last but not least I've decoupled setting up the interval from your counting logic. I've done this with an extra state that switches between true and false. So when you click your button the state switches to true, the effect is run and everything is set up. If you're components unmounts, or you stop the timer, or the secondsPerRep prop changes the old interval is cleared and a new one is set up.
Hope that helps!
Try that. The problem was that you're not using the state that is received by the setCurrentSecond function and the function setInterval don't see the state changing.
const Play = props => {
const [currentSecond, setCurrentSecond] = useState(1);
const [timer, setTimer] = useState();
const onClick = () => {
setTimer(setInterval(() => {
setCurrentSecond((state) => {
if (state < props.secondsPerRep) {
return state + 1;
}
return state;
});
}, 1000));
}
return (
<div>
<div>
<button onClick={onClick} disabled={timer}>Start</button>
<p>{currentSecond}</p>
</div>
</div>
);
}