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>
</>
)
}
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
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])
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
Conveniently, I found that the new React docs has an article on resetting state with Keys
Top comments (9)
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 usinguseEffect
will only change some parts.Anyway, nice post!
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:
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.
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?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...
I thought that whenever parent gets re-render, child gets to re-render unless React.memo is used. This is really informative. π
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 theinitialId
prop is changed. That's because initializinguseState
value is only executed once regardless of states / props are changed.Hopefully that will clear up your understanding π
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.
Yeah, he meant components not using memo:
react.dev/reference/react/memo#ski...