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:
import { useEffect, useState } from "react"
function App() {
const [counter, setCounter] = useState(0)
useEffect(() => {
setCounter(value => value + 1)
})
return <div className="App">{counter}</div>
}
export 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:
import { useEffect, useState } from "react"
function App() {
const [counter, setCounter] = useState(0)
useEffect(() => {
setCounter(value => value + 1)
}, [])
return <div className="App">{counter}</div>
}
export 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:
import { useEffect, useState } from "react"
function App() {
const person = {
name: "John",
age: 23,
}
const [counter, setCounter] = useState(0)
useEffect(() => {
setCounter(value => value + 1)
}, [person])
return <div className="App">{counter}</div>
}
export 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.
import { useEffect, useMemo, useState } from "react"
function App() {
const person = useMemo(
() => ({
name: "John",
age: 23,
}),
[]
)
const [counter, setCounter] = useState(0)
useEffect(() => {
setCounter(value => value + 1)
}, [person])
return <div className="App">{counter}</div>
}
export 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:
import { useEffect, useState } from "react"
function App() {
const getData = () => {
// fetch data
return { foo: "bar" }
}
const [counter, setCounter] = useState(0)
useEffect(() => {
const data = getData()
setCounter(value => value + 1)
}, [getData])
return <div className="App">{counter}</div>
}
export 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.
import { useCallback, useEffect, useState } from "react"
function App() {
const getData = useCallback(() => {
// fetch data
return { foo: "bar" }
}, [])
const [counter, setCounter] = useState(0)
useEffect(() => {
const data = getData()
setCounter(value => value + 1)
}, [getData])
return <div className="App">{counter}</div>
}
export default App
Conditions inside useEffect
Consider a scenario where you want to fetch the data and update the state as shown below:
import { useEffect, useState } from "react"
function Child({ data, setData }) {
useEffect(() => {
// Fetch data
setData({ foo: "bar" })
}, [data, setData])
return <div className="App">{JSON.stringify(data)}</div>
}
export const App = () => {
const [data, setData] = useState()
return <Child data={data} setData={setData} />
}
export 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:
import { useEffect, useState } from "react"
function Child({ data, setData }) {
useEffect(() => {
if (!data) {
// fetch data
setData({ foo: "bar" })
}
}, [data, setData])
return <div className="App">{JSON.stringify(data)}</div>
}
export const App = () => {
const [data, setData] = useState()
return <Child data={data} setData={setData} />
}
export default App
Top comments (1)
Thorough and well explained article, thank you.