Understanding implications of useEffect

4 min read
Table of Contents

Let’s explore how useEffect behaves when state updates involve objects — step by step.

  1. Updating the entire object state.
  2. Updating with the same reference.
  3. Updating and watching inner changes to an object.

Updating state in its entirety

sample.tsx
import { useEffect, useState } from "react"
export default function () {
const [user, setUser] = useState({name: 'ashfaq', age:30, work: { exp: 6}})
useEffect(() => {
console.log('user updated')
}, [user])
useEffect(() => {
const id = setInterval(() => {
setUser({name: 'ashfaq', age:30, work: { exp: 2}})
}, 1000)
return () => clearInterval(id)
}, [])
return <div>
my name is {user.name}
<p>
current: {user.age}
</p>
</div>
}

You can see in the browser console we get the “user updated” message every second though we are updating with the exact same object, this is because we are creating a new object and react sees it as different object as it matches these internally based on address space (Object comparison).

Updating react object using the same object reference

Contrary to earlier example, if we try to update the state referencing the same user object we wouldn’t get any messages in the console.

import {useEffect, useState} from 'react'
export default function Sample() {
const [user, setUser] = useState({name: 'ashfaq', age:30, work: { exp: 2}})
useEffect(() => {
console.log('user updated')
}, [user])
useEffect(() => {
const id = setInterval(() => {
setUser(user)
}, 1000)
return () => clearInterval(id)
}, [])
return <div>
my name is {user.name}
<p>
current: {user.age}
</p>
</div>
}

Watching inner changes of a react state

Now lets watch inner property’s reactivity.

sample.tsx
import { useEffect, useState } from "react"
export default function Sample() {
const [user, setUser] = useState({name: 'ashfaq', age:30, work: { exp: 2}})
useEffect(() => {
console.log('age updated')
}, [user.age])
useEffect(() => {
console.log('user updated')
}, [user])
useEffect(() => {
const id = setInterval(() => {
setUser((x) => ({...x, age: x.age+1}))
}, 2000)
return () => clearInterval(id)
}, [])
return <div>
my name is {user.name}
<p>
current: {user.age}
</p>
</div>
}

In above example we get to see both the prompts “age updated” & “user updated”, understanding this is critical.

Though react allows watching inner state change, we have to be careful as it is essentially replacing the entire object and exposing the state change of both “user” & “user.age”, we should be careful writing code that accommodates only for changes in that variable.

  • For example, if we write code for “user.id” we should make sure that doesn’t impact the changes in “user” as this is going to trigger 2 useEffects.

Extending the same concept for inner states

sample.tsx
import { useEffect, useState } from 'react'
// `app/page.tsx` is the UI for the `/` URL
export default function Page() {
const [user, setUser] = useState({name: 'ashfaq', age:30, work: { exp: 2}})
useEffect(() => {
console.log('age updated')
}, [user.age])
useEffect(() => {
console.log('work updated')
}, [user.work])
useEffect(() => {
console.log('user updated')
}, [user])
useEffect(() => {
const id = setInterval(() => {
setUser((x) => ({...x, work: {exp: 3}}))
}, 2000)
return () => clearInterval(id)
}, [])
return <div>
my name is {user.name}
<p>
current: {user.age}
</p>
</div>
}

Here we see similar logs except that “user updated” is replaced with “work updated” as we are replacing the inner object altogether.

It is important to notice that our useEffect for “user.age” is not updating though we are replacing the entire object.

Now lets try to update the inner objects “exp”

sample.tsx
import { useEffect, useState } from "react"
export default function Sample() {
const [user, setUser] = useState({name: 'ashfaq', age:30, work: { exp: 2}})
useEffect(() => {
console.log('age updated')
}, [user.age])
useEffect(() => {
console.log('work updated')
}, [user.work])
useEffect(() => {
console.log('user updated')
}, [user])
useEffect(() => {
const id = setInterval(() => {
setUser((x) => ({...x, work: {exp: 3}}))
}, 2000)
return () => clearInterval(id)
}, [])
return <div>
my name is {user.name}
<p>
current: {user.age}
</p>
</div>
}

Now we see the console has “work updated”, “user updated” & “work exp updated”, this follows the earlier state update philosphy where useEffect runs of the object change and also the inner property change.

Conclusion

  • Using multiple useEffects on a densely populated state should be done with care, if its too must nesting, maybe a state management library is a better choice.
  • Ideally watching inner changes explicitly when required would be advised as it would not trigger state changes on the object itself.
My avatar

Thanks for reading my blog post! Feel free to check out my other posts or contact me via the social links in the footer.


More Posts