I'm implementing count down
and use useRef hook to using it when clean setTimeout when the user navigates to the next screen to avoid cancel all subscription warning and it's work!
But I have something weird when count - 1 i can see "hey" in the console! although not cleaning the setTimeOut!!
I don't want to clean it in this case but why should loggin every time count changes!
code snippet
const [seconds, setSeconds] = useState(40);
const countRef = useRef(seconds);
useEffect(() => {
if (seconds > 0) {
countRef.current = setTimeout(() => {
setSeconds(seconds - 1);
}, 1000);
} else {
setSeconds(0);
}
return () => {
console.log('hey'); // every count down it's appeared
clearTimeout(countRef.current);
};
}, [seconds]);
You see "hey" because you're using seconds as a dependency. So every time seconds changes, the effect must run again leading to the effect's destroy function (the function you returned from the effect) to be invoked.
Instead of having seconds as a dependency, you should instead have setSeconds.
const [seconds, setSeconds] = React.useState(10);
useEffect(() => {
let didUnsub = false;
const id = setInterval(() => {
setSeconds((prev) => {
// while the count is greater than 0, continue to countdown
if (prev > 0) {
return prev - 1;
}
// once count eq 0, stop counting down
clearInterval(id);
didUnsub = true;
return 0;
});
}, 1000);
return () => {
console.log("unmounting");
// if the count didn't unsubscribe by reaching 0, clear the interval
if (!didUnsub) {
console.log("unsubscribing");
clearInterval(id);
}
};
}, [setSeconds]);
If you look at the example below, you'll see that the effect is only run once, when the component is mounted. If you were to cause the component to dismount, the destroy function would be invoked. This is because the setState is a dispatch function and doesn't change between renders, therefor it doesn't cause the effect to continuously be called.
In the example you can click the button to toggle between mounting and dismounting the counter. When you dismount it notice that it logs in the console.
Example: https://codesandbox.io/s/gallant-silence-ui0pv?file=/src/Countdown.js
Related
I'm trying to understand what my mistake is. I'm using setInterval within useEffect with [] dependency that updates the ms (milliseconds) state by adding 1 to it every 10 milliseconds. I have a time() function that is responsible for updating ms and secs state and also for stopping and displaying the timer. Once the timer reaches 5 seconds, done state is set to true, the interval is cleared and the timer should stop. But instead it crashes with this error: "Too many re-renders. React limits the number of renders to prevent an infinite loop". Why does this happen and how do I fix it? Here's the code link https://codepen.io/Montinyek/pen/zYLzBZP?editors=1111
function App() {
const [secs, setSecs] = React.useState(0);
const [ms, setMs] = React.useState(0);
const [done, setDone] = React.useState(false)
let id = React.useRef()
React.useEffect(() => {
id.current = setInterval(() => {
if (!done) {
setMs(prev => prev += 1)
}
}, 10);
return () => clearInterval(id.current);
}, [])
function time() {
if (ms === 100) {
setMs(0)
setSecs(prev => prev += 1)
}
if (secs === 5) {
clearInterval(id.current)
setDone(true)
}
let formattedSecs = secs < 10 ? "0" + secs : secs;
let formattedMils = ms < 10 ? "0" + ms : ms;
return `${formattedSecs} : ${formattedMils}`;
}
return <div>{time()}</div>;
}
The problem is that you are calling the function time() in render and that function is making calls to set state. Generally, you should never set state in render or you get into a loop situation when you render, the state is set (which triggers a rerender), then state is set again, then it rerenders, and so on.
Your problem isn't actually that new intervals are being created. Its actually unrelated entirely to the timer ticks in a way. The problem is that when it reaches 5 seconds, it gets into a "render loop".
Specifically what happens in your case is this:
The timer hits 5 seconds.
Render calls time()
clearInterval(id.current) is called and also setDone(true). The set operation here causes another render.
Render calls time().
Back to (3).
You need to encapsulate the logic that sets the state in the interval handler, and not make your logic intrinsically linked to render passes. However, this is one of the more complicated things in react (handling state in an interval) since you can get into all sorts of problems with recalling stale state. To understand my answer, you will need to read Dan Abramov's (a key React contributor) article about this. I have lifted the useInterval hook from his blog.
function useInterval(callback, delay) {
const savedCallback = React.useRef();
// Remember the latest callback.
React.useEffect(() => {
savedCallback.current = callback;
}, [callback]);
// Set up the interval.
React.useEffect(() => {
function tick() {
savedCallback.current();
}
if (delay !== null) {
let id = setInterval(tick, delay);
return () => clearInterval(id);
}
}, [delay]);
}
function App() {
const [secs, setSecs] = React.useState(0);
const [ms, setMs] = React.useState(0);
useInterval(() => {
if (ms >= 100) {
setMs(0)
setSecs(prev => prev + 1)
return
}
setMs(prev => prev + 1)
}, secs < 5 ? 10 : null)
function getFormattedTime() {
let formattedSecs = secs < 10 ? "0" + secs : secs;
let formattedMils = ms < 10 ? "0" + ms : ms;
return `${formattedSecs} : ${formattedMils}`;
}
return <div>{getFormattedTime()}</div>;
}
ReactDOM.render(<App />, document.getElementById("root"));
Note that the render now only calls getFormattedTime which does not touch the state.
When refactoring this I found done wasn't needed since useInterval supports conditionally stopping the interval easily by passing a variable tickrate: secs < 5 ? 10 : null. null means "stopped".
I am building a simple clock app with React. Currently the countDown() function works, but I would like the user to be able to stop/start the clock by pressing a button. I have a state boolean called paused that is inverted when the user clicks a button. The trouble is that after the value of paused is inverted, the reference to paused inside the countDown() function passed to setInterval() seems to be accessing the default value of paused, instead of the updated value.
function Clock(){
const [sec, setSecs] = useState(sessionLength * 60);
const [paused, setPaused] = useState(false);
const playPause = () => {
setPaused(paused => !paused);
};
const countDown = () => {
if(!paused){
setSecs(sec => sec - 1)
}
}
useEffect(() => {
const interval = setInterval(() => {
countDown();
}, 1000);
return () => {
clearInterval(interval);
};
}, []);
I'm assuming it has something to do with the asynchronous nature of calls to setState() in React, and/or the nature of scoping/context when using regular expressions. However I haven't been able to determine what is going on by reading documentation related to these concepts.
I can think of some workarounds that would allow my app to function as desired. However I want to understand what is wrong with my current approach. I would appreciate any light anyone can shed on this!
In your code, the useEffect is called only once when mounting the component.
The countdown function registered inside will have its initial value at the time when the useEffect/setInterval is called. So paused will only have the value when you initially mount the component. Because you are not calling countDown directly or updating its value inside your useEffect, it is not updated.
I think you could solve this issue like this:
interval.current = useRef(null);
const countDown = () => {
if(!paused){
setSecs(sec => sec - 1)
}
}
useEffect(() => {
clearInterval(interval.current);
interval.current = setInterval(countDown, 1000);
return () => {
clearInterval(interval.current);
};
}, [paused]);
your useEffect is dependent on the value of paused as it needs to create a new interval (with a different countdown function). This will trigger the useEffect not only on mount but every time paused changes. So one solution would be to clear the interval and start a new one with a different callback function.
Edit: You could actually improve it as you only want the interval to be running if the countDown function actually does something so this should work too:
useEffect(() => {
clearInterval(interval.current);
if(!paused) {
interval.current = setInterval(countDown, 1000);
}
return () => {
clearInterval(interval.current);
};
}, [paused]);
I created a function in which when user click start button timer will start, but it's not working. Can someone tell me why it's not working? please
that's the function I created
const [time,setTime] = useState(0)
const timeout = setInterval(() => {
if (time !== 60) {
setTime(prevState => prevState + 1);
}
}, 1000);
console.log(timeout);
return () => {
if (time == 60) {
clearTimeout(timeout);
}
};```
You could declare the Timer State as 60 instead of 0 and
const [state,updateState] = useState({timer:60})
then call this in updateState: ({timer: timer - 1})
To answer your question:
why is my code not working?
Your state timer starts out being 0 and will therefore never reach inside the if statement.
As Matt U pointed out you most likely want to use setInterval since it runs the function you pass at every X milliseconds (1000 in your case) until you stop it.
See the following for more information regarding that:
setTimeout: https://www.w3schools.com/jsref/met_win_settimeout.asp
setInterval: https://www.w3schools.com/jsref/met_win_setinterval.asp
What yesIamFaded answered should do the job in your use case, though it would be better to make use of updateState's argument prevState (or whatever you want to call it). updateState will receive the previous value and use that to compute a new value.
const [state, updateState] = useState({ timer: 60 })
const interval = setInterval(() => {
if (state.timer > 0) {
updateState(prevState => { timer: prevState.timer - 1 });
}
}, 1000);
You can read more about functional updates here:
https://reactjs.org/docs/hooks-reference.html#functional-updates
And lastly, you should clear the timeout and/or interval once you don't need it anymore using either clearTimeout() or clearInterval().
See the following for more information here:
clearTimeout: https://www.w3schools.com/jsref/met_win_cleartimeout.asp
clearInterval: https://www.w3schools.com/jsref/met_win_clearinterval.asp
P.S.
If your timer state isn't coupled with any other state I wouldn't put it into an object. Instead I would do the following:
const [timer, setTimer] = useState(60)
const interval = setInterval(() => {
if (timer > 0) {
setTimer(prevTimer => prevTimer - 1 );
}
}, 1000);
That way you won't have an unnecessary object.
I have a react component that performs a certain task at regular intervals of time after mounting. But I want to clear the interval once after a criterion is met. How can I achieve that?
My code
const [totalTime, setTotalTime] = React.useState(10000);
const foo = () => {
console.log("Here");
};
React.useEffect(() => {
const secondInterval = setInterval(() => {
if (totalTime > 0) setTotalTime(totalTime - 1000);
}, 1000);
return () => clearInterval(secondInterval);
});
React.useEffect(() => {
let originalInterval;
if (totalTime > 0)
originalInterval = setInterval(() => {
foo();
console.log(totalTime);
}, 5000);
return () => clearInterval(originalInterval);
}, []);
When I watch the console even after 10000ms It is still logging Here and also totalTime is always being 10000ms. I am not able to figure out what exactly is happening.
You may need to pass the older state as an argument to the setTotalTime updater function. You also may need to pass (another) state variable as a dependency to the useEffect hook so that the function is executed every time the state variable changes
React.useEffect(() => {
const secondInterval = setInterval(() => {
if (totalTime > 0) setTotalTime(totalTime => totalTime - 1000);
}, 1000);
return () => clearInterval(secondInterval);
}, [...]);
Depends on your criteria, and what you call a criteria, but you could just use another state and then useEffect on that another state:
const [criteriaIsMet,setCriteriaIsMet] = useState(false);
useEffect(() => { if(criteriaIsMet) {clearInterval(...)} },[criteriaIsMet])
And then somewhere when you do your actual "criteria logic" you just go ahead and do setCriteriaIsMet(true)
And how would you know Id of interval in above useEffect - again you could just create a special state that will store that id, and you could set it in your original useEffect
As for why your current code is not clearing the interval is because when you use useEffect with empty array as second argument, and return function in first function argument - that will be execute on component unmounting
And these are just one of numerous options you have actually :)
I have a function react component that has a counter that starts from 10000 and goes to 0.
I am setting a setInterval callback using useEffect hook during component mounting. The callback then updates the counter state.
But I don't know why, the count value never decreases. Each time the callback runs count is 10000.
(I am using react & react-dom version 16.8.3)
Function component is as below:
import React, { useState, useEffect, useRef } from 'react'
const Counter = () => {
const timerID = useRef()
let [count, setCount] = useState(10000)
useEffect(() => {
timerID.current = setInterval(() => {
//count here is always 10000
if (count - 1 < 0) {
setCount(0)
} else {
setCount(count - 1)
}
}, 1)
}, [])
return <h1 className="counter">{count}</h1>
}
export default Counter
Here is the link to codesandbox: link
You need to watch for changes in count, and also clean up your useEffect():
useEffect(() => {
timerID.current = setInterval(() => {
if (count - 1 < 0) {
setCount(0)
} else {
setCount(count - 1)
}
}, 100)
return () => clearInterval(timerID.current);
}, [count])
As #Pavel mentioned, Dan Abramov explains why here.
There are 2 options:
1) Include count in the dependencies
This is not ideal, as it means a new setInterval will be created on every change of count, so you would need to clean it up on every render, example:
useEffect(() => {
timerID.current = setInterval(() => {
//count here is always 10000
if (count - 1 < 0) {
setCount(0)
} else {
setCount(count - 1)
}
}, 1)
return () => clearInterval(timerID.current) // Added this line
}, [count]) // Added count here
2) Add the count in the setInterval callback function.
This is the best approach for intervals, as it avoids, setting new ones all the time.
useEffect(() => {
timerID.current = setInterval(() => {
// count is used inside the setCount callback and has latest value
setCount(count => {
if (count - 1 < 0) { // Logic moved inside the function, so no dependencies
if (timerID.current) clearInterval(timerID.current)
return 0
}
return count - 1
})
}, 1)
return () => {
if (timerID.current) clearInterval(timerID.current) // Makes sure that the interval is cleared on change or unmount
}
}, [])
Here is the sandbox link
You are declaring effect function when component mount as you said. So in scope in that time value store inside count is equal to 10000. That means every time interval function executes it takes count value from closure (10000). It is actually pretty tough to do it correctly. Dan wrote whole blog post about it