DEV Community

Cover image for A neat little trick to avoid useEffect in React
Shivam Jha
Shivam Jha

Posted on • Originally published at shivamjha.io

A neat little trick to avoid useEffect in React

Problem

I came across a peculiar problem once. For the sake of this article, let's go with this contrived example:

I have a component MyDumbComponent.tsx and it receives an id with initial state value and uses that state to fetch some data.
That state can also be manipulated inside same component:

import { useEffect, useState } from 'react'
import todos from '../data/todos.json'

type Unpacked<T> = T extends (infer U)[] ? U : T

export default function MyDumbComponent({ initialId }: { initialId: number }) {
  const [id, setId] = useState(initialId)
  const [todoData, setTodoData] = useState<Unpacked<typeof todos> | null>(null)

  useEffect(() => {
    const allTodos = todos
    const selectedTodo = allTodos.find(todo => todo.id === id) ?? null
    setTodoData(selectedTodo)
  }, [id])

  return (
    <>
      <div>
        <code>
          <pre>{JSON.stringify(todoData, null, 2)}</pre>
        </code>
      </div>
      <small>Child id: {id}</small>
      <br />
      <br />
      <button onClick={() => setId(prev => prev + 1)}>+</button>
      <button onClick={() => id > 1 && setId(prev => prev - 1)}>-</button>
    </>
  )
}
Enter fullscreen mode Exit fullscreen mode

When I clicked on + and - button, it would change the id and will fetch a new todo detail and show it to user.
See Code example demo

This worked perfectly fine and as expected. The issue came when I wanted to update the id(props) in parent.
My dumb common sense would say it should also re-render. But to my suprise it didn't. Here I updated the state:

import { useState } from 'react'
import './App.css'
import MyDumbComponent from './components/MyDumbComponent'

function App() {
  const [count, setCount] = useState(1)

  return (
    <div>
      <p> Parent state variable: {count} </p>
      <button onClick={() => setCount(c => c + 1)}>
        Increment parent state
      </button>
      <br /> <br />
      <br />
      <MyDumbComponent initialId={count} />
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Here it can be seen React does not 'reload' child component if the prop is used as an initial value for state, even when the prop gets changed

At first, it can be unintuitive. The reason is that React updates a component when it's state changes or it's props changes.
If React just throw away and child and re-make a new one everytime one of it's props gets changed, it would also have to create new DOM nodes, create new ones, and set those.
These can be expensive, specially if the props change frequently or has a large number of such changing props. All of that is expensive and slow. So, React will re-use the component that was there since it's the same type and at the same position in the tree.

Using useEffect as a state updater

I am guilty of using a second effect in this scenario😅
It would like: Hmm.. so we need to do something based on when the prop is changed.. what gets fired when the prop changed... useEffect with that prop in dependency!!
So, I would add this effect after the 1st one(imo the first useEffect should be relaced with react-query or some other data fetching lib, too).
But none-the-less, this is how that would go:

useEffect(() => {
  // Changing children's state whenever our prop `initialId` changes
  setId(initialId)
}, [initialId])
Enter fullscreen mode Exit fullscreen mode

Here it can be seen this appraoch of using an useEffect (tongue-twister, right?) to update the vale of state initialized with some prop works

But this solution can be better. The useEffect updated the value of state in 2nd render.
Also, it is a good rule of thumb to prevent using useEffect as long as one can. I have noticed this increases readability and prevent some bugs with not-very-cared use of useEffect.
This advice has helped me remembering this: useEffect should only be used when an external service / something outside the React paradigm (like custom listeners) need to be integrated with React.
So useEffect can be thought of as useSyncronise

Solution: using Keys to "reload" a React Component

So, what is the way? Keys to the rescue!!🔑 If a component has a key and it changes, React skips comparion and makes new fresh component
So you can consider Keys as an "identity" or source of truth, if you will, for a component. Hence, if the Key changes, the component must be reloaded from scratch.
This is the same reason you need keys while rendering a list, so that React can differentiate you list items when (if) their position / order changes within that list.

So, in our case, we can just pass the key to child component and it will be recreated from scratch:

import { useState } from 'react'
import './App.css'
import MyDumbComponent from './components/MyDumbComponent'

function App() {
  const [count, setCount] = useState(1)

  return (
    <div>
      <p> Parent state variable: {count} </p>
      <button onClick={() => setCount(c => c + 1)}>
        Increment parent state
      </button>
      <br /> <br />
      <br />
      <MyDumbComponent key={count} initialId={count} />
    </div>
  )
}

export default App
Enter fullscreen mode Exit fullscreen mode

Conveniently, I found that the new React docs has an article on resetting state with Keys

Further Read:

Top comments (9)

Collapse
 
maafaishal profile image
Muhammad A Faishal

In this case, it seems that using useEffect is more optimized than resetting state with key (React re-creates the component (and all of its children) from scratch.). Since using useEffect will only change some parts.

Anyway, nice post!

Collapse
 
shivamjjha profile image
Shivam Jha

Thanks!

useEffect can be more optimised in the sense that it will just fetch and update the single todo without recreating the entire component. But in this case and 90% of others, this will be very cheap.
Also, useEffect takes another render to update state, so that’s 2 extra renders on every change in id.

The new react docs has a section on resetting state using keys:

react.dev/reference/react/useState...

Taking this into consideration + the long term effects of using useEffect hook, resetting with keys is preferable most of the time.

React docs has entire sections dedicated to prevent using useEffect:

Collapse
 
testcoverrage profile image
Katz En • Edited

I'd go further than it being more optimized, though; as stated this is a classic case for useReducer.

This scenario opens with the smart/dumb metaphor, and then performs state work in the child. In said design metaphor, the child UI state should be derivative of a parent-accessible application state. An mechanism should exist in the parent that receives an event whenever an interaction or interesting change happens in the child. The smart component then performs the necessary work to decide whether or not the upstream state gets updated, which results in the pertinent UI on the parent, as well as the state, UI on the child updating. QED.

Collapse
 
lionelrowe profile image
lionel-rowe

You'll end up with stale state in the parent if you update from the child, though. Why not just pass the setCount function as a prop to the child?

Collapse
 
shivamjjha profile image
Shivam Jha

This is not updating the child state when parent is updated. This is resetting the child’s state when we want to.

This is a contrived example. Suppose we had 10 such states in child, we would not want all of them to be lifted up to the parent:

react.dev/reference/react/useState...

Collapse
 
husnain profile image
Husnain Mustafa

I thought that whenever parent gets re-render, child gets to re-render unless React.memo is used. This is really informative. 👍

Collapse
 
maafaishal profile image
Muhammad A Faishal • Edited

The Child component will always re-render whenever the Parent component re-renders.

The issue raised by the author is that the id state is never reset even though the initialId prop is changed. That's because initializing useState value is only executed once regardless of states / props are changed.

Hopefully that will clear up your understanding 🙂

Collapse
 
husnain profile image
Husnain Mustafa

Understood.

You said, "The Child component will always re-render whenever the Parent component re-renders."
But, as per my knowledge, Child component do not render if we use React.memo. It will only re-render when its props get changed in React.memo case. So even if parent gets re-rendered, if props are same for the child, child would not re-render.

Thread Thread
 
shivamjjha profile image
Shivam Jha

Yeah, he meant components not using memo:

react.dev/reference/react/memo#ski...