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.
Related
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);
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.
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);
};
}, []);
I've got a little problem there and I dont know why my solution is not working properly.
Here is the code:
const [progress, setProgress] = useState(0);
useEffect(() => {
let isMounted = true;
if(isMounted === true) {
progress < 100 && setTimeout(() => setProgress(progress + 1), 20);
}
return () => isMounted = false;
}, [progress]);
Im doing there setTimeout async operation. Every 20ms i want to set state of progress by 1. Thats all. Also I added isMounted variable which contains state of component.
The problem is that when lets say, I mount this component and I unmount this
immediately after 1s maybe two then i dont get this error.
If I wait longer and unmount the component (before setTimeout has time to change the progress state to 100, which is the limit) then this error appears.
Why this error is appearing in such weird way?
Why does this error even appear when the component has clearly communicated when it is mounted and when not?
You need to either clear the timeout in the cleanup or use your isMounted variable within the timeout itself.
Clearing the timeout:
useEffect(() => {
let timeout;
if (progress < 100) {
timeout = setTimeout(() => {
setProgress(progress + 1)
}, 20);
}
return () => { clearTimeout(timeout) };
}, [progress]);
Using the isMounted variable:
useEffect(() => {
let isMounted = true;
if (progress < 100) {
setTimeout(() => {
if (isMounted) setProgress(progress + 1);
}, 20)
}
return () => { isMounted = false };
}, [progress]);
const [timer,setTimer] = useState()
const [number, setNumber] = useState()
const [list, setlist] = useState([])
const numberChange = (number)=>{
setNumber(number)
if (!(list.find(item=>item===number))){
setlist([...list,number])}
}
const randomNumber=()=> 1+Math.floor(Math.random()*90)
const randNumberChange=()=>{
let randNumber = randomNumber()
if (list.find(item=>item===randNumber))
randNumberChange()
else
numberChange(randNumber)
}
const startTimer = () => {
setTimer(setInterval(()=>{
randNumberChange()
}, 5000))
}
const stopTimer=()=>{
clearInterval(timer)
}
The list is always rendering only one item and not appending it.
When randNumberChange is called separately then the list gets appended but not with setInterval.
When startTimer funcion is executed is stopped with stopTimer and then started again it appends second item then stop and it repeats
Change setlist([...list,number])} to setlist((prevState) => [...prevState, number]). React set state is async in nature. So to get the correct list value from the state you would need to get the value from previous state. Doc
Suggestion: that instead of setting timer in state, you can start the interval in useEffect.
Also in numberChange function, you should get the list from previous state and then append the new number in that. This will make sure that the list value is updated before adding new number.
import React, { Component, useState } from "react";
import { render } from "react-dom";
import Hello from "./Hello";
import "./style.css";
const Test = () => {
const [number, setNumber] = useState(null);
const [list, setlist] = useState([]);
const numberChange = number => {
setNumber(number);
if (!list.find(item => item === number)) {
setlist((prevState) => [...prevState, number]);// instead of directly using list value, get it from previous state
}
};
const randomNumber = () => 1 + Math.floor(Math.random() * 90);
const randNumberChange = () => {
console.log("here");
let randNumber = randomNumber();
if (list.find(item => item === randNumber)) randNumberChange();
else numberChange(randNumber);
};
const startTimer = () => {
return setInterval(() => { randNumberChange(); }, 5000);
}
const stopTimer = (timer) => {
clearInterval(timer)
}
React.useEffect(() => {
const timer = startTimer();
return ()=> stopTimer(timer);
}, []);
console.log(list);
return <div>{number}</div>;
};
You'll need to use useEffect hook:
useEffect(() => {
// You don't need timer state, we'll clear this later
const interval = setInterval(() => {
randNumberChange()
},5000)
return () => { // clear up
clearInterval(interval)
}
},[])
use useEffect with setTimeout it is work as setInterval
useEffect(() => {
setTimeout(() => setList([...list, newValue]), 2000)
}, [list])