DEV Community

Cover image for How to solve Infinity loop in React's useEffect?
collegewap
collegewap

Posted on • Updated on • Originally published at codingdeft.com

How to solve Infinity loop in React's useEffect?

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Latest comments (1)

Collapse
 
aderchox profile image
aderchox

Thorough and well explained article, thank you.