Timer goes crazy when using setInterval inside useEffect - javascript

So I've been trying to learn the fundamentals of React and tried something like this which made the code go crazy. It looks like all the digits change wildly fast starting from the initial one and after a while the browser gives warnings like
"setInterval" is taking 205ms
Tried adding timer as a dependency and even returning it. It's all a little confusing to me. Can someone please explain to me in depth what's happening here?
import {useState, useEffect} from 'react'
function App() {
const [timer, setTimer] = useState(0)
useEffect(()=>{
setInterval(()=>{
setTimer(timer + 1)
}, 1000)
},[timer])
return (
<>
<h1>Timer : {timer}</h1>
</>
);
}
export default App;
Please explain

You need to remove timer as dependency form the useEffect otherwise it will recreate another interval every time the timer gets updated.
You may also want to clear the interval using clearInterval in the cleanup of the hook i.e. when component unmounts (in this case).
And, as state updates may be asynchronous, you need to do setTimer(prev => prev + 1) to read the previous (the latest state) and increment to it.
function App() {
const [timer, setTimer] = React.useState(0)
React.useEffect(() => {
const id = setInterval(() => {
setTimer((prev) => prev + 1)
}, 1000)
return () => {
clearInterval(id)
}
}, [])
return <h1>Timer : {timer}</h1>
}
ReactDOM.render(<App />, document.getElementById('mydiv'))
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<body>
<div id="mydiv"></div>
</body>

Related

Infinity state&component updating

I tried to create simple timer with useEffect and setInterval. In theory, on click it has to:
restart cooldown
const restartCooldown = () => {
setCooldownDuration(targetActivity['duration'])
}
useEffect() will see this and decrease cooldown every second within interval
useEffect(() => {
let interval
if (props.activity.cooldown) {
if (cooldownDuration > 0) {
if (props.activity.id === 1) {
console.log('Activity.js state', cooldownDuration)
}
interval = setInterval(() => setCooldownDuration(cooldownDuration - 1000), 1000);
} else {
clearInterval(interval)
}
} else {
setCooldownDuration(0)
}
});
useEffect() in ActivityProgression.js will get percentage of completion
useEffect(() => {
const interval = setInterval(() => setPercentages((props.totalTime - props.cooldown) / props.totalTime * 100), 1000);
})
4. Then it will be rendered
return <ProgressBar animated now={percentages} label={`${percentages}%`} />;
But in fact it doesn't work properly, creating infinity multiple renders on click.
Can you please tell me why? Much much thanks.
Full code repository to try it yourself
I tried everything, but it seems that i simply don't understand some react features :(
The second argument of useEffect is an array of dependencies. Since you didn't provide any dependencies, it runs every render. Because it updates the state, it causes a re-render and thus runs again, infinitely. Add [cooldownDuration] to your use effect like this:
useEffect(() => {...}, [cooldownDuration])
This will make the useEffect only run when cooldownDuration changes.

Related to weird behaviour of react useState and useEffect

import {useEffect,useState} from 'react';
export default function App() {
const [count,setCount]=useState(0);
const [flag,setFlag]=useState(false);
function increment(){
setCount(prevState=>{
if(flag)
return prevState
return prevState+1;
});
}
useEffect(function(){
increment();
setFlag(true);
increment();
},[]);
return (
<div className="App">
{count}
</div>
);
}
Was playing around with effects and states in reatct functional component, I expected the code to output "1" but it's giving the output as "2", Why is it happening and How can I make it print 1 ?
Once you call setFlag, React will update the returned value of your useState call [flag,_] = useState() on the next render.
Your setFlag(true) call schedules a re-render, it doesn't immediately update values in your function.
Your flag is a boolean const after all -- it can't be any value but one value in that function call.
How to solve it gets interesting; you could put the the flag inside of a single state object i.e. useState({count: 0, flag: false})
But more likely, this is an academic problem. A count increment sounds like something that would trigger on a user interaction like a click, and so long as one function doesn't call increment() multiple times (this sounds unusual), the re-render will happen in time to update your flag state.
For performance reasons, React defers useState hook updates until function completes its execution, i.e. run all statements in the function body and then update the component state, so React delays the update process until a later time.
Thus, when increment function execution is completed, React updates the state of count. But for setFlag method, the execution environment is a context of useEffect hook's callback, so here React's still waiting for a completion of useEffect's callback function. Therefore, inside the callback of useEffect the value of flag is still false.
Then you again called your increment function and when this function finished its execution, your count again was incremented by 1.
So, in your case, the key factor is the way of deferring state updates until function execution by React.
Think of setState() as a request rather than an immediate command to update the component. For better perceived performance, React may delay it, and then update several components in a single pass. React does not guarantee that the state changes are applied immediately.
setState() does not always immediately update the component. It may batch or defer the update until later. This makes reading this.state right after calling setState() a potential pitfall. Instead, use componentDidUpdate or a setState callback (setState(updater, callback)), either of which are guaranteed to fire after the update has been applied. If you need to set the state based on the previous state, read about the updater argument below.
React Component: setState()
For more information, you can also read about Batch updates in React (especially in React 18) or Reactive programming (this is not React), where the main idea is real-time or timely updates.
For a better understanding I would think it of as replacing the invocation directly with setter and we know how the state batching works so ...
import { useEffect, useState } from "react";
export default function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
useEffect(function () {
// increment(); becomes below
setCount((prevState) => {
if (flag) return prevState;
return prevState + 1;
});
// queued update count returns
// count => count + 1 0 0 + 1 = 1
setFlag(true);
//set flag=true in next render
// increment(); becomes below
setCount((prevState) => {
if (flag) return prevState;
return prevState + 1;
});
// so flag is still false here and count is 1
// queued update count returns
// count => count + 1 1 1 + 1 = 2
// done and count for next render is 2 and flag will be false
}, []);
return <div className="App">{count}</div>;
A better explaination in Docs - Queueing state updates and state as snapshot
State updates are "batched". See the other answers for an explanation. Here's a workaround using useRef - since a ref can be updated during this render, you can use it like a "normal" variable.
const { useState, useRef, useEffect } = React;
function App() {
const [count, setCount] = useState(0);
const flag = useRef(false);
function increment() {
setCount(prevState => {
if (flag.current)
return prevState;
return prevState + 1;
});
}
useEffect(function() {
increment();
flag.current = true;
increment();
}, []);
return <div className="App">{count}</div>;
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.2.0/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.2.0/umd/react-dom.production.min.js"></script>
<div id="root"></div>

Why setState in setTimeout not batching?

Code:
useEffect(() => {
setTimeout(() => {
// this cause re-render twice
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
}, 1000);
}, []);
My question is why this component re-render twice if we call two setCount in sequence.
Doesn't React batch multiple setCount in one go?
Thank you for answering or any suggestion.
codesandbox example
Doesn't React batch multiple setCount in one go?
It does if it can. This is a case where it can not.
In order for react to batch things, the execution of code needs to start within react itself. For example, any component lifecycle events, or any synthetic event callbacks (eg, the onClick for a <button>). When that happens, react can let your code keep running, queuing up as many set states as you like, and then once your done you will return to react's code, which can then render the queued changes.
But if code execution did not start from react, then once you return, you're not going to be returning to react's code. So since they won't get to run code after yours, they do the render right away.
React will batch state updates if they're triggered from within a React-based event, like from inside a event handler or if they are called synchronously. It will not batch updates if they're triggered outside of React, like a setTimeout() or a Promise callback.
In your example the setCount is called from the context of a timer hence the batch updates did not happen. But if you call setCount multiple times from the context of a event handler or called synchronously, the state updates will be batched as shown in the snippet below.
Notice that the timer and the promise callbacks do not batch the updates:
function App() {
const [count, setCount] = React.useState(0);
console.log("re-render");
React.useEffect(() => {
setTimeout(() => {
// This cause re-render twice
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
}, 2000);
// Will be batched as it it called
// In the context of React
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
//The state updates from the promise callback
//Will not be batched
Promise.resolve(1).then(data => {
console.log("Promise Resolved");
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
});
}, []);
const clickHandler = () => {
// Will be batched
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
};
return (
<div className="App">
<h1>{count}</h1>
<button onClick={clickHandler}>Click</button>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(
<React.StrictMode>
<App />
</React.StrictMode>,
rootElement
);
<script crossorigin src="https://unpkg.com/react#17/umd/react.development.js"></script>
<script crossorigin src="https://unpkg.com/react-dom#17/umd/react-dom.development.js"></script>
<div id="root"></div>
The other answers explained why it is rendering several times.
Now, I want to give a solution to make it render only once. There is an API that allows you to make only one render: ReactDOM.unstable_batchedUpdates
In your example:
ReactDOM.unstable_batchedUpdates(() => {
setCount((prev) => prev + 1);
setCount((prev) => prev + 1);
});
}, 1000);
https://reactjs.org/blog/2022/03/08/react-18-upgrade-guide.html#automatic-batching
Now, it's something React handles itself.

How to cancel setTimeout when navigate to another page?

Good evening, could you please advise a solution to cancel setTimeout when user navigate to other pages (for example, click other links, press back button of browser, but not change to other tabs). I have tried window event "unload", but it seems not work as expected.
My app is a normal count down, if it count to 0, it will automatically navigate to assigned page. For some purpose, I need to disable this automatic navigate if user click on other links while it is counting. Thank you so much.
import React, { useEffect } from 'react';
import {useHistory} from "react-router-dom";
const SucessPurchaseSubmit = () => {
const history = useHistory();
const navigateTo = () => history.push("/house-catalog");
useEffect(() => {
const time = document.querySelector(".time");
let count = 10;
var timer;
// automatic navigate to full catalog after 10 seconds
function countToNavigate(){
clearTimeout(timer);
time.innerHTML = count;
if (count === 0) {
navigateTo();
}
count -= 1;
timer = setTimeout(countToNavigate, 1000)
}
countToNavigate();
window.addEventListener("unload", () => {
clearTimeout(timer);
})
})
return (
<section className="success-purchase-submit">
<h1>Thank you so much for your information</h1>
<h3>One of our consultants will contact you very shortly</h3>
<h5>In the mean time, we will back to Full Catalog automatically after:</h5>
<h5 className="time">10</h5>
</section>
);
};
export default SucessPurchaseSubmit;
I may have found a solution for your problem.
const FunctionalComponent = () => {
React.useEffect(() => {
return () => {
console.log("Bye");
};
}, []);
return <h1>Bye, World</h1>;
};
This is referred from - https://www.twilio.com/blog/react-choose-functional-components
You can use clearInterval() inside the useEffect().
The useEffect() works similar to the "OG" componentWillUnmount(). This is the perfect place to clearInterval() once redirecting to new page.
According to https://reactjs.org/docs/hooks-reference.html#cleaning-up-an-effect , you can return a function from useEffect to do clean up when the component is unmounted.
So, at the end of the useEffect function, you can probably add:
return () => clearTimeout(timer);
To cancel the timeout when the component is removed.

React Hooks: State always setting back to initial value

Switching over to react hooks and using them for the first time. My state always seems to be set back to the initial value I pass it (0). Code is to have a page automatically scroll down and up. The page is just practice to displaying various file types. What happens is the scrollDir variable will switch to being set to either 1 or -1 and 0. So the console will display 1,0,1,0,1,0,1,0 etc... How do I get the state to stay during an update?
function App(props) {
const [scrollDir, setScrollDir] = useState(0);
function scrollDown() {
if(document.documentElement.scrollTop < 10)
{
setScrollDir(1);
}
else if(document.documentElement.scrollTop >= document.documentElement.scrollHeight - window.innerHeight)
{
setScrollDir(-1);
}
window.scrollBy(0, scrollDir);
}
useEffect(() => {
setInterval(scrollDown, 100);
});
return (
<StackGrid monitorImagesLoaded={true} columnWidth={"33.33%"} >
<img src={posterPNG} />
<img src={posterJPG} />
<img src={posterGIF} />
<video src={posterMP4} loop autoPlay muted />
<Document file={posterPDF}>
<Page pageNumber={1} />
</Document>
</StackGrid>
);
}
useEffect hook takes second argument, an array.
If some value in that array changes then useEffect hook will run again
If you leave that array empty useEfect will run only when component mount - (basicaly like ComponentDidMount lifecycle method)
And if you omit that array useEffect will run every time component rerenders
For example:
useEffect runs only once, when component mounts:
useEffect(() => {
//code
},[]);
useEffect runs everytime when component rerenders:
useEffect(() => {
//code
});
useEffect runs only when some of these variables changes:
let a = 1;
let b = 2;
useEffect(() => {
//code
},[a,b]);
Also, if you set return statement in useEffect hook, it has to return a function, and that function will always run before useEffect render
useEffect(() => {
// code
return () => {
console.log("You will se this before useEffect hook render");
};
}, []);
Using setInterval with hooks isn't very intuitive. See here for an example: https://stackoverflow.com/a/53990887/3984987. For a more in-depth explanation you can read this https://overreacted.io/making-setinterval-declarative-with-react-hooks.
This useInterval custom hook is pretty easy to use as well https://github.com/donavon/use-interval. (It's not mine - I ran across this after responding earlier)

Categories