How to solve the React useEffect Hook’s infinite loop patterns
Matt Angelosanto
Posted on May 10, 2022
Written by Hussain Arif✏️
React’s useEffect
Hook lets users work on their app’s side effects. Some examples can be:
- Fetching data from a network: often, applications fetch and populate data on the first mount. This is possible via the
useEffect
function - Manipulating the UI: the app should respond to a button click event (for example, opening a menu)
- Setting or ending timers: if a certain variable reaches a predefined value, an inbuilt timer should halt or start itself
Even though usage of the useEffect
Hook is common in the React ecosystem, it requires time to master it. Because of this, many newbie developers configure their useEffect
function in such a way that it causes an infinite loop problem. In this article, you will learn about the infamous infinite loop and how to solve it.
Let’s get started!
What causes infinite loops and how to solve them
Passing no dependencies in a dependency array
If your useEffect
function does not contain any dependencies, an infinite loop will occur.
For example, look at the following code:
function App() {
const [count, setCount] = useState(0); //initial value of this
useEffect(() => {
setCount((count) => count + 1); //increment this Hook
}); //no dependency array.
return (
<div className="App">
<p> value of count: {count} </p>
</div>
);
}
useEffect
by default triggers on every update cycle if there are no dependencies. As a result, the app here will execute the setCount
function upon every render. So, this causes an infinite loop:
What causes this issue?
Let’s break down our issue step by step:
- On the first render, React checks the value of
count
. Here, sincecount
is0
, the program executes theuseEffect
function - Later on,
useEffect
invokes thesetCount
method and updates the value of thecount
Hook - After that, React re-renders the UI to display the updated value of
count
- Furthermore, since
useEffect
runs on every render cycle, it re-invokes thesetCount
function - Since the above steps occur on every render, this causes your app to crash
How to fix this issue
To mitigate this problem, we have to use a dependency array. This tells React to call useEffect
only if a particular value updates.
As the next step, append a blank array as a dependency like so:
useEffect(() => {
setCount((count) => count + 1);
}, []); //empty array as second argument.
This tells React to execute the setCount
function on the first mount.
Using a function as a dependency
If you pass a method into your useEffect
dependency array, React will throw an error, indicating that you have an infinite loop:
function App() {
const [count, setCount] = useState(0);
function logResult() {
return 2 + 2;
}
useEffect(() => {
setCount((count) => count + 1);
}, [logResult]); //set our function as dependency
return (
<div className="App">
<p> value of count: {count} </p> {/*Display the value of count*/}
</div>
);
}
In this snippet, we passed our logResult
method into the useEffect
array. In theory, React only has to increment the value of count
on the first render.
What causes this issue?
- One thing to remember is that
useEffect
uses a concept called shallow comparison. It does this to verify whether the dependency has been updated - Here, the problem is that during each render, React redefines the reference of
logResult
- As a result, this re-triggers the
useEffect
function upon each cycle - Consequently, React calls the
setCount
Hook until your app encounters an Update Depth error. This introduces bugs and instability into your program
How to fix this issue
One solution to this is to use the useCallback
Hook. This allows developers to memoize their function, which ensures that the reference value stays the same. Due to the stable reference value, React shouldn’t re-render the UI infinitely:
const logResult = useCallback(() => {
return 2 + 2;
}, []); //logResult is memoized now.
useEffect(()=> {
setCount((count)=> count+1);
},[logResult]); //no infinite loop error, since logResult reference stays the same.
This will be the result:
Using an array as a dependency
Passing an array variable into your dependencies will also run an infinite loop. Consider this code sample:
const [count, setCount] = useState(0); //iniital value will be 0.
const myArray = ["one", "two", "three"];
useEffect(() => {
setCount((count) => count + 1); //just like before, increment the value of Count
}, [myArray]); //passing array variable into dependencies
In this block, we passed in our myArray
variable into our dependency argument.
What causes this issue?
Since the value of myArray
doesn’t change throughout the program, why is our code triggering useEffect
multiple times?
- Here, recall that React uses shallow comparison to check if the dependency’s reference has changed.
- Since the reference to
myArray
keeps on changing upon each render,useEffect
will trigger thesetCount
callback - Therefore, due to
myArray's
unstable reference value, React will invokeuseEffect
on every render cycle. Eventually, this causes your application to crash
How to fix this issue
To solve this problem, we can make use of a useRef
Hook. This returns a mutable object which ensures that the reference does not change:
const [count, setCount] = useState(0);
//extract the 'current' property and assign it a value
const { current: myArray } = useRef(["one", "two", "three"]);
useEffect(() => {
setCount((count) => count + 1);
}, [myArray]); //the reference value is stable, so no infinite loop
Passing an object as a dependency
Using an object in your useEffect
dependency array also causes the infinite loop problem.
Consider the following code:
const [count, setCount] = useState(0);
const person = { name: "Rue", age: 17 }; //create an object
useEffect(() => {
//increment the value of count every time
//the value of 'person' changes
setCount((count) => count + 1);
}, [person]); //dependency array contains an object as an argument
return (
<div className="App">
<p> Value of {count} </p>
</div>
);
The result in the console indicates that the program is infinite looping:
What causes this issue?
- Just like before, React uses shallow comparison to check if the reference value of
person
has changed - Since the reference value of the
person
object changes on every render, React re-runsuseEffect
- As a result, this invokes
setCount
on every update cycle. This means that we now have an infinite loop
How to fix this issue
So how do we get rid of this problem?
This is where useMemo
comes in. This Hook will compute a memoized value when the dependencies change. Other than that, since we have a memoized variable, this ensures that the state’s reference value does not change during each render:
//create an object with useMemo
const person = useMemo(
() => ({ name: "Rue", age: 17 }),
[] //no dependencies so the value doesn't change
);
useEffect(() => {
setCount((count) => count + 1);
}, [person]);
Passing an incorrect dependency
If one passes the wrong variable into the useEffect
function, React will throw an error.
Here is a brief example:
const [count, setCount] = useState(0);
useEffect(() => {
setCount((count) => count + 1);
}, [count]); //notice that we passed count to this array.
return (
<div className="App">
<button onClick={() => setCount((count) => count + 1)}>+</button>
<p> Value of count{count} </p>
</div>
);
What causes this issue?
- In the above code, we are telling to update the value of
count
within theuseEffect
method - Furthermore, notice that we passed the
count
Hook to its dependency array as well - This means that every time the value of
count
updates, React invokesuseEffect
- As a result, the
useEffect
Hook invokessetCount
, thus updatingcount
again - Due to this, React is now running our function in an infinite loop
How to fix this issue
To get rid of your infinite loop, simply use an empty dependency array like so:
const [count, setCount] = useState(0);
//only update the value of 'count' when component is first mounted
useEffect(() => {
setCount((count) => count + 1);
}, []);
This will tell React to run useEffect
on the first render.
Conclusion
Even though React Hooks are an easy concept, there are many rules to remember when incorporating them into your project. This will ensure that your app stays stable, optimized, and throws no errors during production.
Furthermore, recent releases of the Create React App CLI also detect and report infinite loop errors during runtime. This helps developers spot and mitigate these issues before they make it onto the production server.
Thank you so much for reading! Happy coding!
Full visibility into production React apps
Debugging React applications can be difficult, especially when users experience issues that are hard to reproduce. If you’re interested in monitoring and tracking Redux state, automatically surfacing JavaScript errors, and tracking slow network requests and component load time, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens on your React app. Instead of guessing why problems happen, you can aggregate and report on what state your application was in when an issue occurred. LogRocket also monitors your app's performance, reporting with metrics like client CPU load, client memory usage, and more.
The LogRocket Redux middleware package adds an extra layer of visibility into your user sessions. LogRocket logs all actions and state from your Redux stores.
Modernize how you debug your React apps — start monitoring for free.
Posted on May 10, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.
Related
November 29, 2024