I'm a little puzzled here and would appreciate if somebody could explain why useState exhibits this behaviour. I have a functional component using useState which starts a timer and renders the time correctly in real time in the the DOM. Upon stopping the timer I'd like to push the time to an array, but every attempt to do so simply pushed 0 - the initial state of the time variable.
After some debugging I noticed that if I console.log() the time inside of the interval-looped function it also continues to log 0, and not the "real" time.
Here's my code. I've cut out all of the parts irrelevent to the problem at hand.
export default function Timer() {
const [time, setTime] = useState(0);
const interval = useRef(null);
clearInterval(interval.current);
let startTime = Date.now() - time;
interval.current = setInterval(() => {
setTime(Date.now() - startTime);
console.log(time); // <-- Why does this continue to show 0?
}, 10);
return (
<div>
<span>{("0" + Math.floor((time / 60000) % 1000)).slice(-2)}:</span>
<span>{("0" + Math.floor((time / 1000) % 1000)).slice(-2)}.</span>
<span>{("00" + (time % 1000)).slice(-3, -1)}</span>
</div>
);
}
So my question is, why does the time variable return the correct time in real time inside of the DOM but not in the console? I thought it may be due to useState not being instant so to speak, but I don't get why it would just continue logging 0.
EDIT:
I fixed this issue by declaring a separate variable curTime = 0 and instead of using setTime(Date.now() - startTime), I used curTime = (Date.now() - startTime):
export default function Timer() {
const [time, setTime] = useState(0);
const interval = useRef(null);
let curTime = 0
clearInterval(interval.current);
let startTime = Date.now() - time;
interval.current = setInterval(() => {
curTime = (Date.now() - startTime);
setTime(curTime)
console.log(curTime); // <-- Now shows correct time.
}, 10);
return (
<div>
<span>{("0" + Math.floor((time / 60000) % 1000)).slice(-2)}:</span>
<span>{("0" + Math.floor((time / 1000) % 1000)).slice(-2)}.</span>
<span>{("00" + (time % 1000)).slice(-3, -1)}</span>
</div>
);
}
let startTime = Date.now() - time;
interval.current = setInterval(() => {
setTime(Date.now() - startTime);
console.log(time); // <-- Why does this continue to show 0?
}, 10);
From what I understand, you are trying to print the updated state value in the following line of setting the state. This will not work in the general case. The reason being the setter of the state is asynchronous in nature. It means, anytime you run setTime/setState, it will be called by React at that place itself but its updated value will only be visible on the next render. That is why your DOM shows an updated value (after every rerender). Always remember, your function execution is synchronous so the following line will be executed at that time itself but the updated state will be visible on the next render.
Having said that React team realized there might be cases when state update has to behave synchronously.
For that, they designed 'flushSync'. You can check more here:
https://reactjs.org/docs/react-dom.html#flushsync (This is not recommended by React team and should only be used when async updates of state don't help.)
React uses a declarative style. The value of the state will change when the component rerenders. You are logging before that happens.
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 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 this countdown script that I made in react that calculates the time until an event and it works fine, but I want it to recalculate every 1 second to check for updates and such. How do I do this, code inserted below..
const { DateTime } = require("luxon");
// ...
export function GetLaunchCountdown(time) {
// Get launch time
const launchTime = DateTime.fromISO(time.time);
// Get current time
const currentTime = DateTime.now()
// Calculate time difference
const timeDiff = launchTime.diff(currentTime)
const timeDiffMili = timeDiff.toMillis()
var displayTime = DateTime.fromMillis(timeDiffMili)
if (timeDiff.toMillis() > 86400000) {
// Display with days
return(<>{displayTime.toFormat("dd:hh:mm:ss")}</>)
} else {
// Display with hours
return(<>{displayTime.toFormat("hh:mm:ss")}</>)
}
}
// ...
It would be easiest to just subtract from the timeDiffMili variable (that's why it's there).
Store the current time in the state. Use a timing function, like setInterval to change the state. Trigger that timing function inside a useEffect function so that you get a single interval running (and don't start a new one ever rerender). Return a function that clears the interval when the component is unmounted.
const [currentTime, setCurrentTime] = useState(DateTime.now())
useEffect( () => {
const update = () => {
setCurrentTime(DateTime.now());
}
const interval = setInterval(update, 500);
return () => clearInterval(interval);
}, []);
can you guys help me with this? I want to render my countdown timer HH:MM:SS. However, my data to the state. My setInterval was working but when I tried to add setState and pass the data for rendering it gives me an error. Unhandled Rejection (Error): Maximum update depth exceeded. This can happen when a component repeatedly calls setState inside componentWillUpdate or componentDidUpdate. React limits the number of nested updates to prevent infinite loops.
Also, here's my code:
componentDidMount() {
this.props.dispatch(questActions.getAll());
this.props.dispatch(userActions.getAll(1, 100000));
this.getTimeRemaining();
this.interval = setInterval(() => {
this.getTimeRemaining();
}, 1000)
}
componentWillUnmount() {
clearInterval(this.interval)
}
getTimeRemaining() {
const { quests } = this.props || {}
const questLists = quests?.items?.items;
let today = moment();
if (typeof questLists !== 'undefined') {
questLists.forEach(questTimer => {
let currentDate = today;
let expiredDate = moment(questTimer.expiresAt);
let timeRemaining = duration(expiredDate.diff(currentDate))
let hours = timeRemaining.hours();
let minutes = timeRemaining.minutes();
let seconds = timeRemaining.seconds();
//Adding to array to render display
questTimer.hours = hours;
questTimer.minutes = minutes;
questTimer.seconds = seconds;
this.setState({
hours,
minutes,
seconds
})
})
}
}
You have an infinite loop. You are probably doing some things when the component updates on hours, minutes or seconds. Also, you are calling the setState function every single second. Are you sure you want to call setInterval ? What is the purpose behind this to connect to the redux store every single second to make a dispatch? Maybe a setTimeout will be suffice?
Side note:
If something updates in the redux store, you don't need a dispatch to get its value. It can also be passed as a prop to your component and your component will update itself.
I'm trying to create a timer with the code below, but when I console log time in the tick function all I get back is 60 continuously , if I pass the console.log after time - 1 I get NaN , which I guess means that time for some reason is not processed as an integer.
I cannot use state as using state for this timer re renders my components again and again which makes everything go crazy.
componentWillMount() {
var time = 60;
this.interval = setInterval(this.tick.bind(this, time), 1000);
}
tick(time) {
time = Number.parseInt(time) // This does nothing for some reason
console.log(time);
time = time - 1;
if (time <= 0) {
console.log('Hi');
} return time;
}
EDIT: I found your actual problem.
You need to store time in state. Add it to your initial state as 60, change the interval line to:
setInterval(this.tick, 1000)
and your tick function to:
tick = () => {
this.setState((prevState) => {
return {
time: prevState.time - 1
}
}, () => {
if(this.state.time <= 0){
console.log('hi')
}
})
}
This way, time is tracked outside of your function scope and the value should always be correct. By passing an anonymous callback as the second argument to setState you ensure that the check only happens after time is decremented. The one issue I'd see with this is that React can be a bit funky with running setState and may not run every second, so this could end up being slightly imprecise.
componentWillMount() {
var time = 60;
this.interval = setInterval(
(function() {
time--;
this.tick(time);
}).bind(this),
1000);
}
tick(time) {
time = Number.parseInt(time) // This does nothing for some reason
console.log(time);
//time = time - 1; //I removed this line
if (time <= 0) {
console.log('Hi');
}
return time;
}
componentWillMount would look a lot prettier if you used arrow functions
componentWillMount() {
let time = 60;
this.interval = setInterval(() => {
time--;
this.tick(time);
},
1000);
}
Now you are changing time in the right place and sending it on to the function on each call.