React JS 18 : 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(<App>) 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 <C /> 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 <D /> 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 <App> 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!

Rules of React Rendering

One of the primary rules of React rendering is that rendering must be "pure" and not have any side effects! This can be tricky and confusing, because many side effects are not obvious, and don't result in anything breaking. For example, strictly speaking a console.log() statement is a side effect, but it won't actually break anything. Mutating a prop is definitely a side effect, and it might not break anything. Making an AJAX call in the middle of rendering is also definitely a side effect, and can definitely cause unexpected app behavior depending on the type of request.

Sebastian Markbage wrote an excellent document entitled The Rules of React. In it, he defines the expected behaviors for different React lifecycle methods, including render, and what kinds of operations would be considered safely "pure" and which would be unsafe. It's worth reading that in its entirety, but I'll summarize the key points:

 

  • Render logic must not:
    • Can't mutate existing variables and objects
    • Can't create random values like Math.random() or Date.now()
    • Can't make network requests
    • Can't queue state updates
  • Render logic may:
    • Mutate objects that were newly created while rendering
    • Throw errors
    • "Lazy initialize" data that hasn't been created yet, such as a cached value

Component Metadata and Fibers

React stores an internal data structure that tracks all the current component instances that exist in the application. The core piece of this data structure is an object called a "fiber", which contains metadata fields that describe:

  • What component type is supposed to be rendered at this point in the component tree
  • The current props and state associated with this component
  • Pointers to parent, sibling, and child components
  • Other internal metadata that React uses to track the rendering process

You can see the definition of the Fiber type as of React 17 here.

During a rendering pass, React will iterate over this tree of fiber objects, and construct an updated tree as it calculates the new rendering results.

Note that these "fiber" objects store the real component props and state values. When you use props and state in your components, React is actually giving you access to the values that were stored on the fiber objects. In fact, for class components specifically, React explicitly copies componentInstance.props = newProps over to the component right before rendering it. So, this.props does exist, but it only exists because React copied the reference over from its internal data structures. In that sense, components are sort of a facade over React's fiber objects.

Similarly, React hooks work because React stores all of the hooks for a component as a linked list attached to that component's fiber object. When React renders a function component, it gets that linked list of hook description entries from the fiber, and every time you call another hook, it returns the appropriate values that were stored in the hook description object (like the state and dispatch values for useReducer.

When a parent component renders a given child component for the first time, React creates a fiber object to track that "instance" of a component. For class components, it literally calls const instance = new YourComponentType(props) and saves the actual component instance onto the fiber object. For function components, React just calls YourComponentType(props) as a function.