A (Mostly) Complete Guide to React Rendering Behavior · Mark’s Dev Blog


Details on how React rendering behaves, and how use of Context and React-Redux affect rendering

I’ve seen a lot of ongoing confusion over when, why, and how React will re-render components, and how use of Context and React-Redux will affect the timing and scope of those re-renders. After having typed up variations of this explanation dozens of times, it seems it’s worth trying to write up a consolidated explanation that I can refer people to. Note that all this information is available online already, and has been explained in numerous other excellent blog posts and articles, several of which I’m linking at the end in the “Further Information” section for reference. But, people seem to be struggling to put the pieces together for a full understanding, so hopefully this will help clarify things for someone.

Table of Contents

What is “Rendering”?

Rendering is the process of React asking your components to describe what they want their section of the UI to look like, now, based on the current combination of props and state.

Rendering Process Overview

During the rendering process, React will start at the root of the component tree and loop downwards to find all components that have been flagged as needing updates. For each flagged component, React will call either classComponentInstance.render() (for class components) or FunctionComponent() (for function components), and save the render output.

A component’s render output is normally written in JSX syntax, which is then converted to React.createElement() calls as the JS is compiled and prepared for deployment. createElement returns React elements, which are plain JS objects that describe the intended structure of the UI. Example:

// This JSX syntax:
return Text here

// is converted to this call:
return React.createElement(SomeComponent, {a: 42, b: "testing"}, "Text Here")

// and that becomes this element object:
{type: SomeComponent, props: {a: 42, b: "testing"}, children: ["Text Here"]}

After it has collected the render output from the entire component tree, React will diff the new tree of objects (frequently referred to as the “virtual DOM”), and collects a list of all the changes that need to be applied to make the real DOM look like the current desired output. The diffing and calculation process is known as “reconciliation”.

React then applies all the calculated changes to the DOM in one synchronous sequence.

Render and Commit Phases

The React team divides this work into two phases, conceptually:

  • The “Render phase” contains all the work of rendering components and calculating changes
  • The “Commit phase” is the process of applying those changes to the DOM

After React has updated the DOM in the commit phase, it then synchronously runs the componentDidMount and componentDidUpdate class lifecycle methods, and the useLayoutEffect hooks.

React then sets a short timeout, and when it expires, runs all the useEffect hooks.

You can see a visualization of the class lifecycle methods in this excellent React lifecycle methods diagram. (It does not currently show the timing of effect hooks, which is something I’d like to see added.)

In React’s upcoming “Concurrent Mode”, it is able to pause the work in the rendering phase to allow the browser to process events. React will either resume, throw away, or recalculate that work later as appropriate. Once the render pass has been completed, React will still run the commit phase synchronously in one step.

A key part of this to understand is that “rendering” is not the same thing as “updating the DOM”, and a component may be rendered without any visible changes happening as a result. When React renders a component:

  • The component might return the same render output as last time, so no changes are needed
  • In Concurrent Mode, React might end up rendering a component multiple times, but throw away the render output each time if other updates invalidate the current work being done

How Does React Handle Renders?

Queuing Renders

After the initial render has completed, there are a few different ways to tell React to queue a re-render:

  • Class components:
    • this.setState()
    • this.forceUpdate()
  • Function components:
    • useState setters
    • useReducer dispatches
  • Other:
    • Calling ReactDOM.render() again (which is equivalent to calling forceUpdate() on the root component)

Standard Render Behavior

It’s very important to remember that:

React’s default behavior is that when a parent component renders, React will recursively render all child components inside of it!

As an example, say we have a component tree of A > B > C > D, and we’ve already shown them on the page. The user clicks a button in B that increments a counter:

  • We call setState() in B, which queues a re-render of B.
  • React starts the render pass from the top of the tree
  • React sees that A is not marked as needing an update, and moves past it
  • React sees that B is marked as needing an update, and renders it. B returns as it did last time.
  • C was not originally marked as needing an update. However, because its parent B rendered, React now moves downwards and renders C as well. C returns again.
  • D was also not marked for rendering, but since its parent C rendered, React moves downwaard and renders D too.

To repeat this another way:

Rendering a component will, by default, cause all components inside of it to be rendered too!

Also, another key point:

In normal rendering, React does not care whether “props changed” – it will render child components unconditionally just because the parent rendered!

This means that calling setState() in your root component, with no other changes altering the behavior, will cause React to re-render every single component in the component tree. After all, one of the original sales pitches for React was “act like we’re redrawing the entire app on every update”.

Now, it’s very likely that most of the components in the tree will return the exact same render output as last time, and therefore React won’t need to make any changes to the DOM. But, React will still have to do the work of asking components to render themselves and diffing the render output. Both of those take time and effort.

Remember, rendering is not a bad thing – it’s how React knows whether it needs to actually make any changes to the DOM!

Improving Rendering Performance

Having said that, it’s also true that that render work can be “wasted” effort at times. If a component’s render output didn’t change, and that part of the DOM didn’t need to be updated, then the work of rendering that component was really kind of a waste of time.

React component render output should always be entirely based on current props and current component state. Therefore, if we know ahead of time that a component’s props and state haven’t changed, we should also know that the render output would be the same, that no changes are necessary for this component, and that we can safely skip the work of rendering it.

When trying to improve software performance in general, there are two basic approaches: 1) do the same work faster, and 2) do less work. Optimizing React rendering is primarily about doing less work by skipping rendering components when appropriate.

Component Render Optimization Techniques

React offers three primary APIs that allow us to potentially skip rendering a component:

  • React.Component.shouldComponentUpdate: an optional class component lifecycle method that will be called early in the render process. If it returns false, React will skip rendering the component. It may contain any logic you want to use to calculate that boolean result, but the most common approach is to check if the component’s props and state have changed since last time, and return false if they’re unchanged.
  • React.PureComponent: since that comparison of props and state is the most common way to implement shouldComponentUpdate, the PureComponent base class implements that behavior by default, and may be used instead of Component + shouldComponentUpdate.
  • React.memo(): a built-in “higher order component” type. It accepts your own component type as an argument, and returns a new wrapper component. The wrapper component’s default behavior is to check to see if any of the props have changed, and if not, prevent a re-render. Both function components and class components can be wrapped using React.memo(). (A custom comparison callback may be passed in, but it really can only compare the old and new props anyway, so the main use case for a custom compare callback would be only comparing specific props fields instead of all of them.)

All of these approaches use a comparison technique called “shallow equality”. This means checking every individual field in two different objects, and seeing if any of the contents of the objects are a different value. In other words, obj1.a === obj2.a && obj1.b === obj2.b && ......... This is typically a fast process, because === comparisons are very simple for the JS engine to do. So, these three approaches do the equivalent of const shouldRender = !shallowEqual(newProps, prevProps).

There’s also a lesser-known technique as well: if a React component returns the exact same element reference in its render output as it did the last time, React will skip re-rendering that particular child.

For all of these techniques, skipping rendering a component means React will also skip rendering that entire subtree, because it’s effectively putting a stop sign up to halt the default “render children recursively” behavior.

How New Props References Affect Render Optimizations

We’ve already seen that by default, React re-renders all nested components even if their props haven’t changed. That also means that passing new references as props to a child component doesn’t matter, because it will render whether or not you pass the same props. So, something like this is totally fine:

function ParentComponent() {
    const onClick = () => {
      console.log("Button clicked")
    }
    
    const data = {a: 1, b: 2}
    
    return 
}

Every time ParentComponent renders, it will create a new onClick function reference and a new data object reference, then pass them as props to NormalChildComponent. (Note that it doesn’t matter whether we’re defining onClick using the function keyword or as an arrow function – it’s a new function reference either way.)

That also means there’s no point in trying to optimize renders for “host components”, like a

or a





Source link

Leave a Reply

Your email address will not be published. Required fields are marked *