React rendering

What is “render”?

The main job React was designed to do is to keep in sync state and UI. It does it by keeping the snapshot of virtual elements in memory. render in context of React is a way to figure out what has changed in the snapshot and apply only those changes to the actual DOM or UI tree in case of React Native. React decides if any updates to DOM are needed by taking previously rendered version and comparing it to the new version returned from render function.

Rendering is split into two phases:

  1. Render - determines what has changed by calling render() and comparing result with stored virtual DOM tree.
  2. Commit - applies changes. In case of web version React changes the actual html by modifying DOM.

Despite the popular belief react doesn’t re-render on props change. Every render in React begins with changing state. This causes the component itself and all its descendants to re-render. As state changes propagate down the tree, some descendant components receive new recalculated props. It creates a misconception that render of such components was caused by changes in props. In fact it was caused by changes to the state on the higher levels.

Batching

In practice modern React doesn’t re-render after each state change and batches state updates into one when possible. Say you you’re changing two pieces of state in single callback handler:

const [firstName, setFirstName] = useState(null);
const [lastName, setLastName] = useState(null);

const fullNameUpdateCallback = useCallback(() => {
  setFirstName("John");
  setLastName("Doe");
}, []);

In the above example two state updates inside fullNameUpdateCallback will be batched and apply at once. In fact it doesn’t matter where individual state change functions reside in the application. React will enable batching for them despite the location.

There is also new hook useTransition() allowing to distinguish more important updates from the less important ones.

Pure components

By default when component re-renders it also re-renders its descendants. It doesn’t matter if you pass the changed state as props to descendant components or not. There is a way to play smart and skip renders in such situations unless the props change. For that reason we can use so called pure components. Those are the components that skip re-renders when given the same props. React.memo and React.PureComponent give us the possibility to ignore re-renders when state updates but props values stay the same. They achieve it by utilizing memoization technique. React compares passed props shallowly but it can be tweaked by passing a second argurment to React.memo or using shouldComponentUpdate in case of React.PureComponent. Components using such optimizations would not render when parent does. They will only re-render if their properties change.

When utilizing memoization it’s important to wrapp the callbacks into useCallback and other objects into useMemo. Otherwise if they’re re-created on each render, meaning the reference to object in memory changes, it will break memoization.

Both methods come in handy when you have more complex components doing a lot of work internally or producing deep complex render tree. Abusing memoisation can actually make the wrapped component slower. Render typically happens so fast that sometimes it’s faster to re-render rather than comparing all the values in props.

Optimize re-renders with children

As we know state change causes descendants of the component to re-render. Not all of the descendants can be fast to re-render.

code of dashboard with slow component

In this example toggling expanded state of header will cuase the entire component to re-render including <ItemList />. We can avoid expensive re-render simply by composing components differently.

code of slow component passed as children

In this example slow <ItemList /> is passed as children to Dashboard. State changes in Dashboard wouldn’t affect this component anymore. It wouldn’t need to re-render.

In fact you can pass item list as any prop, it not necessary need to be children.

code of slow component passed as an element

Props whouldn’t be affected by state changes so you can use as many of them as needed.

Context

Whenever you write to context provider it causes consumers to re-render. As we also know when parent re-renders, all the children re-render too. It means that update in context could mean all the descendants re-render. The changes to context could not be prevented with memoization techniques we discussed above.

It’s also important to place the context provider as high as possible in the component tree. Otherwise any re-renders caused by ancestors can trigger re-render of the context. As a result all the consumers will be re-rendered as well.

context value without memoization

To prevent it, values in the context provider should be memoized.

context value memoized

References

  1. Why React Re-Renders
  2. A (Mostly) Complete Guide to React Rendering Behavior