Updating (re-rendering) is an important feature of React – when a user interacts with an application, React needs to re-render and update the UI in response to the user’s input. But why does React re-render? If we don’t know why React re-renders, how can we avoid additional re-rendering?
Image From: Render and Commit - React Beta Docs
TL; DR
State changes are one of the only reasons why updates occur inside the React tree.
This statement is an axiom of React updates and there are no exceptions. This article will also be centered around explaining this statement. To avoid getting into ambiguity, this sentence introduces some restrictive definitions and keywords.
Explanation of terms
“Update” and “Rerender”
In React, “update” and “re-render” are two closely related, but completely different, terms. Here’s how to get the correct meaning of these two words.
React’s “update” consists of three phases: Render, which uses createElement or jsx-runtime to generate a new React Element object and assemble a React tree; Reconcilation, where the React Reconciler compares the newly generated React tree with the Reconcilation, where the React Reconciler compares the newly generated React tree with the current React tree and determines the most efficient way to “update” it; and Commit, where the Host (e.g. DOM, Native, etc.) is manipulated to bring the new UI to the user.
Most developers confuse “Update” with “Render” because of the three phases mentioned above, only the “Render” phase is under developer control (Reconcilation and Commit are controlled by react-reconciler
and React Host respectively). In the rest of this article, Reconcilation
always refers to the Rendering
phase of a React component when it is Updated
, and Update
always refers to the entire process of (re)rendering, Reconcilation and Commit.
“React Tree” and “React Tree Inside”
The React Tree itself can be updated at any time. In fact, if you’ve ever learned React through the React documentation, you’ve seen this pattern in the “Hello World” chapter.
|
|
The render
provided by the ReactDOM is called once per second to make a complete update of the entire React tree. But most of the time, you won’t be updating the entire React tree, but rather some of the components within the React tree (in a React application, you’ll only call createRoot().render
or hydrateRoot()
once).
The only reason
If you are using React class components, then you can update a component using the forceUpdate
method that inherits from `React.
So, we can also rephrase this to say that if all class components in a React tree do not use the forceUpdate method, then a state change is the only reason for an update to occur inside the React Tree.
Before the body begins, let’s put out a very confusing quote.
Myth 0: React components are updated for three reasons: state changes, prop changes, and Context changes.
If you ask some developers who use React “why does React update/re-render”, you’ll probably get this answer. That’s true, but it doesn’t reflect the real React update mechanism.
This article will only cover why React updates happen, not how to avoid “unnecessary” updates (maybe I’ll write a separate article on this topic?).
State updates and one-way data flow
Let us take the example of a counter.
|
|
In this example, we declare three components, the root component <App />
renders <Counter />
, and <Counter />
renders <BigNumber />
. In the <Counter />
component, we declare a state count
within the component that changes when the button is clicked, incrementing the state count
.
When we click the button, setCount
is called, the count
state changes, and React updates the <Counter />
component. And when React updates a component, it also updates all the child components under that component (more on why soon). So when the <Counter />
component is updated, the subcomponent <BigNumber />
is also updated.
Now let’s clear up one of the simplest misconceptions.
Misconception 1: When a state changes, the entire React tree is updated.
A few developers using React will believe this (thankfully not most!). . In reality, when state changes, React only updates the component that “owns this state” and all the children of that component.
Why is the parent component (in this case, <App />
is the parent of <Counter />
) not getting updated? Because the main task of React is to keep the state inside React in sync with the UI rendered by React, and React updates that by figuring out how to change the UI so that it’s in sync with the new state. And in React, data is passed top-down in one direction (The Data Flows Down). In this example, the state count
of the <Counter />
component flows down to the prop number
of the <BigNumber />
component, but not up to the <App />
component. Therefore, the <App />
component does not need to be updated when the state of count
changes.
When the count
state changes, the <Counter />
component and its subcomponent <BigNumber />
are updated. And when the <BigNumber />
component is updated, the new value of prop number
is used for rendering. So is the <BigNumber />
component updated because of the change in prop number
?
No, it has nothing to do with props at all
Misconception 2: One of the reasons a React component is updated is because its prop has changed.
Now let’s modify the above example.
|
|
The <SomeDecoration />
component does not accept any prop and does not use the count
state of its parent <Counter />
, but when the count
state changes, the <SomeDecoration />
component is still updated. When a component is updated, React updates all child components, regardless of whether the child component accepts a prop: React is not 100% sure if the <SomeDecoration />
component is directly/indirectly dependent on the count
state.
Ideally, every React component should be a pure function – a “pure” React component that always renders the same UI when fed the same props. But reality is bleak, and it’s very easy to write a React component that’s “impure”.
|
|
The <CurrentTime />
component does not accept any prop, but the UI is rendered differently each time.
Components that include state (using useState
) are also not pure components: even if the prop does not change, the component will render a different UI depending on the state.
Sometimes it’s hard to tell if a component is a pure component or not. You might pass a Ref as a prop to a component (forwardRef
, useImperativeHandle
, cases like that). Ref itself is Reference Stable, and React doesn’t know if the value in the Ref has changed.
React’s goal is to show the latest, consistent UI. to avoid showing the user outdated UI, React updates all child components when the parent component is updated, even if the child component does not accept any prop. props have nothing to do with component updates.
Pure components and memo
You are probably familiar with (or at least have heard of) React.memo
, shouldComponentUpdate
or React.PureComponent
, tools that allow us to `ignore updates:'
|
|
When we wrap the declaration of the <SomeDecoration />
component in memo
, what we’re actually doing is telling React “Hey! I think this is a pure component, so as long as its prop doesn’t change, let’s not update it”.
Now, let’s wrap both <SomeDecoration />
and <BigNumber />
in a memo and see what happens.
|
|
Now, when the count
state is updated, React updates the <Counter />
component and all its children, <BigNumber />
and <SomeDecoration />
. Since <BigNumber />
accepts a prop number
, and the value of number
changes, <BigNumber />
is updated. But the prop of <SomeDecoration />
does not change (because no prop is accepted), so React skips the update of <SomeDecoration />
.
So you’re thinking, why doesn’t React default to all components being pure components? Why doesn’t React memo
all components? In fact, the overhead of updating React components is not as big as you might think. Take the <SomeDecoration />
component for example, it only needs to render one <div />
.
Do you remember what “rendering” means? Go back and look if you don’t.
If a component accepts a lot of complex prop, it’s possible that the performance overhead of rendering the component and comparing it to the Virtual DOM is even less than the overhead of comparing all the prop in a shallow way. Most of the time, React is fast enough. Therefore, we only need to wrap a pure component in memo
if it has a lot of pure subcomponents, or if it has a lot of complex computations inside the pure component.
When a component wrapped in
memo
usesuseState
,useReducer
, oruseContext
, the component will still be updated when the state within the component changes.
Another reason why React does not memo all components by default is that it is very difficult and impractical for React to determine all the dependencies of a child component in Runtime to skip unnecessary updates to the child component. The best time to calculate child component dependencies is during compilation. For more details on this idea, check out Xuan Huang’s talk React without memo at React Conf 2021 .
Let’s talk about Context
Myth 3: One of the reasons React components update is because the value of the
Context.Provider
has been updated.
If we say that when a component is updated due to a state change, all its children are updated with it. It should come as no surprise that when the state we pass through the Context changes, all the children subscribed to the Context are updated.
For pure components, a Context can be considered a “hidden”, or “internal” prop.
In the above example, the <User />
component is a pure component that does not accept any prop, does not use useState
, and does not have any side effects. However, the <User />
component depends on the UserContext
. When the state saved by UserContext
changes, the <User />
component is updated as well.
It is well known that when the value of a Context changes, all the children of <Context.Provider />
will be updated. So why are the children of a Context updated even if they don’t depend on it? Context itself is not a state management tool, but a state passing tool, and the root cause of a change in the value of a Context is a change in state.
|
|
As in the example above, the CountContext changed because the count
state of the <Counter />
component changed; it was not just the consumer component of CountContext
(and its children) that was updated, but all the children of <Counter />
as well.
Reference
https://beta.reactjs.org/learn/render-and-commit
https://medium.com/@guptagaruda/react-hooks-understanding-component-re-renders-9708ddee9928
https://blog.skk.moe/post/react-re-renders-101/