Table of Contents
Have you started using the useEffect hook recently and encountered the following error?
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
This error might be tricky to fix. In this article, we will see different scenarios in which the above error can occur and will see how to fix the infinity loop caused by useEffect.
No dependency array
Consider the below code:
1import { useEffect, useState } from "react"23function App() {4 const [counter, setCounter] = useState(0)5 useEffect(() => {6 setCounter(value => value + 1)7 })89 return <div className="App">{counter}</div>10}1112export default App
In the above code, we are calling setCounter inside the useEffect hook and the counter increments. As the state changes, the component gets re-rendered and useEffect runs again and the loop continues.
The useEffect hook runs again as we did not pass any dependency array to it and causes an infinite loop.
To fix this, we can pass an empty array []
as a dependency to the useEffect hook:
1import { useEffect, useState } from "react"23function App() {4 const [counter, setCounter] = useState(0)5 useEffect(() => {6 setCounter(value => value + 1)7 }, [])89 return <div className="App">{counter}</div>10}1112export default App
Now if you run the app, the useEffect will be called only once in production and twice in development mode.
Objects as dependencies
Consider the following code:
1import { useEffect, useState } from "react"23function App() {4 const person = {5 name: "John",6 age: 23,7 }89 const [counter, setCounter] = useState(0)10 useEffect(() => {11 setCounter(value => value + 1)12 }, [person])1314 return <div className="App">{counter}</div>15}1617export default App
In the above code, we are passing the person
object in the dependency array and
the values within the object do not change from one render to another. Still. we end up in an infinite loop.
You might wonder why.
The reason is, that each time the component re-renders, a new object is created. The useEffect hook checks if the 2 objects are equal. As 2 objects are not equal in JavaScript, even though they contain the same properties and values, the useEffect runs again causing infinite loop.
If you closely observe, you will see the following warning given by the ESLint in VSCode:
The 'person' object makes the dependencies of useEffect Hook (at line 12) change on every render. Move it inside the useEffect callback. Alternatively, wrap the initialization of 'person' in its own useMemo() Hook.eslintreact-hooks/exhaustive-deps
To fix it, we can wrap the object declaration inside a useMemo hook. When we declare an object inside a useMemo hook, it does not get recreated in each render unless the dependencies change.
1import { useEffect, useMemo, useState } from "react"23function App() {4 const person = useMemo(5 () => ({6 name: "John",7 age: 23,8 }),9 []10 )1112 const [counter, setCounter] = useState(0)13 useEffect(() => {14 setCounter(value => value + 1)15 }, [person])1617 return <div className="App">{counter}</div>18}1920export default App
Alternatively, you can also specify the individual properties in the dependency array as [person.name, person.age]
.
Functions as dependencies
The below code will also cause an infinite loop:
1import { useEffect, useState } from "react"23function App() {4 const getData = () => {5 // fetch data6 return { foo: "bar" }7 }8 const [counter, setCounter] = useState(0)9 useEffect(() => {10 const data = getData()11 setCounter(value => value + 1)12 }, [getData])1314 return <div className="App">{counter}</div>15}1617export default App
Here also the ESLint will warn us with the following message:
The 'getData' function makes the dependencies of useEffect Hook (at line 12) change on every render. Move it inside the useEffect callback. Alternatively, wrap the definition of 'getData' in its own useCallback() Hook.eslintreact-hooks/exhaustive-deps
Like how it happened for objects, even function gets declared each time causing an infinite loop.
We can fix the infinite loop by wrapping the function inside useCallback hook, which will not re-declare the function until the dependencies change.
1import { useCallback, useEffect, useState } from "react"23function App() {4 const getData = useCallback(() => {5 // fetch data6 return { foo: "bar" }7 }, [])8 const [counter, setCounter] = useState(0)9 useEffect(() => {10 const data = getData()11 setCounter(value => value + 1)12 }, [getData])1314 return <div className="App">{counter}</div>15}1617export default App
Conditions inside useEffect
Consider a scenario where you want to fetch the data and update the state as shown below:
1import { useEffect, useState } from "react"23function Child({ data, setData }) {4 useEffect(() => {5 // Fetch data6 setData({ foo: "bar" })7 }, [data, setData])89 return <div className="App">{JSON.stringify(data)}</div>10}1112export const App = () => {13 const [data, setData] = useState()14 return <Child data={data} setData={setData} />15}1617export default App
Here as well, you will end up in an infinite loop. You could prevent it by putting a condition inside the useEffect to check if data does not exist. If it doesn't, then only fetch the data and update the state:
1import { useEffect, useState } from "react"23function Child({ data, setData }) {4 useEffect(() => {5 if (!data) {6 // fetch data7 setData({ foo: "bar" })8 }9 }, [data, setData])1011 return <div className="App">{JSON.stringify(data)}</div>12}1314export const App = () => {15 const [data, setData] = useState()16 return <Child data={data} setData={setData} />17}1819export default App
Do follow me on twitter where I post developer insights more often!
Leave a Comment