DEV Community

roggc
roggc

Posted on

Setting state for parent from within useEffect hook in child component causes an infinite loop

That's exactly what I have found recently.
Let's say we have a parent and a child and we pass setState function to the child in order to it can set state for parent from within a useEffect hook inside child component. This scenario will cause an infinite loop no matter what you put in the dependencies' second argument array of useEffect hook.
Let's say what in my opinion happens. setState causes the parent to re-render because we are updating its state. But this implies a render of the child. And I say render and not re-render because when parent re-renders, for useEffect hook is like rendering of the child was first render, and that's why no matter what you put on the dependencies array it will always execute its side effect, setting state for parent and initiating a new loop, that will continue forever.
So when you lift the state up in React.js you must take care not to call setState or dispatch (this applies as well to useReducer) inside a useEffect hook from within a child component.
Here, I show you the code:

import React,{useState} from 'react'
import s from 'styled-components'
import {Ein} from './ein/ein'
import iState from './state'

export const App=()=>{
  const[state,setState]=useState(iState)

  console.log('render app')

  const Div=s.div`
  `

  const el=<Div><Ein state={state} setState={setState}/></Div>

  return el
}
Enter fullscreen mode Exit fullscreen mode

Previous is app component that calls to a child component in order to render it, and passes to it the setState function. Now we look at the ein component definition:

import React,{useEffect} from 'react'
import s from 'styled-components'

export const Ein=({state,setState})=>{
  const Div=s.div`
  `

  console.log('render ein',state.hey)

  useEffect(()=>{
    console.log('useEffect')
    setState({
      ...state,
      hey:true
    })
  },[])

  const el=<Div></Div>
  return el
}
Enter fullscreen mode Exit fullscreen mode

Previous is ein component, the child component for app component. Do not pay too much attention to the details of the state object. It doesn't matter. The thing is we are setting the state for parent component from within a useEffect hook inside child component, and this will inevitably cause an infinite loop.
If we change the location of the useEffect hook and call it from the parent component instead of the child component, the infinite loop disappears.

import React,{useState,useEffect} from 'react'
import s from 'styled-components'
import {Ein} from './ein/ein'
import iState from './state'

export const App=()=>{
  const[state,setState]=useState(iState)

  console.log('render app')

  const Div=s.div`
  `

  useEffect(()=>{
    console.log('useEffect')
    setState({
      ...state,
      hey:true
    })
  },[])

  const el=<Div><Ein state={state} setState={setState}/></Div>

  return el
}
Enter fullscreen mode Exit fullscreen mode

and

import React,{useEffect} from 'react'
import s from 'styled-components'

export const Ein=({state,setState})=>{
  const Div=s.div`
  `

  console.log('render ein',state.hey)

  const el=<Div></Div>
  return el
}
Enter fullscreen mode Exit fullscreen mode

Now we don't have anymore an infinite loop.
That is even more clear if we use useRef to create a var where to store if it's the first render or not:

import React,{useEffect,useRef,useState} from 'react'
import s from 'styled-components'

export const Ein=({state,setState})=>{
  const Div=s.div`
  `

  const [state2,setState2]=useState({count:0})

  console.log('render ein')

  const isFirstRender= useRef(true)

  useEffect(()=>{
    console.log('isFirstRender',isFirstRender.current)
    if(isFirstRender.current){
      isFirstRender.current=false
    }
    setState({
      ...state,
      hey:true
    })
  },[])

  const el=<Div></Div>
  return el
}
Enter fullscreen mode Exit fullscreen mode

You see how we receive as prop in child component the setState function from parent and also declare a new setState2 function local to the child component.
When we use the setState function from parent in the useEffect hook that's what we get in the console:
Alt Text
That is, we get an infinite loop because it always is the first render, while if we use the local setState2 function as in here:

import React,{useEffect,useRef,useState} from 'react'
import s from 'styled-components'

export const Ein=({state,setState})=>{
  const Div=s.div`
  `

  const [state2,setState2]=useState({count:0})

  console.log('render ein')

  const isFirstRender= useRef(true)

  useEffect(()=>{
    console.log('isFirstRender',isFirstRender.current)
    console.log('count',state2.count)
    if(isFirstRender.current){
      isFirstRender.current=false
    }
    setState2({
      ...state2,
      count:state2.count<5?state2.count+1:state2.count
    })
  },[state2.count])

  const el=<Div></Div>
  return el
}
Enter fullscreen mode Exit fullscreen mode

we get this in the javascript console:
Alt Text
As you can see we do not get anymore an infinite loop and useEffect works properly because it is not anymore the first render each time.
Thank you.

Top comments (6)

Collapse
 
opshack profile image
Pooria A

Seems quite normal and expected to me. Changing the state in the child will trigger another render for the child because state is sent as a prop to it and it will repeat again.

useEffect in the way you used it is equivalent to componentDidMount

Collapse
 
roggc profile image
roggc • Edited

Either if you don't pass state as a prop you still get an infinite loop. I think when parent component re-renders, child component unmount and mounts, so is, as you say, componentDidMount, so that's why useEffect code always executes.

Collapse
 
opshack profile image
Pooria A

It would be much better to put the sample codes rather than explaining codes.

Collapse
 
roggc profile image
roggc

Ok, give me five-ten minutes to do that. Thank you.

Collapse
 
jgiron730 profile image
Julio Girón

But in a nutshell, is there no way to update the parent State from the child component inside the useEffect. And if it exist, how would it be?

Collapse
 
roggc profile image
roggc

I think I found the reason why this happens. It is because we are using objects as state. When you set setState({...state,hey:true}) you are setting state to a new object, which has a new address, so state changes, so it re-renders, while if you set state as this for example setState(true), this would not cause an infinite loop because it will not cause a re-render of the App component when setting its state to true when it is already true.