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
}
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
}
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
}
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
}
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
}
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:
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
}
we get this in the javascript console:
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)
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
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 whyuseEffect
code always executes.It would be much better to put the sample codes rather than explaining codes.
Ok, give me five-ten minutes to do that. Thank you.
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?
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 examplesetState(true)
, this would not cause an infinite loop because it will not cause a re-render of theApp
component when setting its state totrue
when it is alreadytrue
.