A pattern-based guide that you can reference on a regular basis when writing components or fixing performance problems.
This is the third part of the series where we dive deep into the concept of re-rendering in React. If you haven't checked out parts one and two yet, do yourself a favour and give 'em a read. Here are the links:
Today, we're cutting straight to the chase - no fluff, no long explanations, just the good stuff. We're talking patterns to stop your React app from re-rendering itself into oblivion. You will or have already encountered these patterns a lot if you are a React developer.
Creating components inside render function of another component is an anti-pattern that can be the biggest performance killer. On every re-render React will re-mount this component (i.e. destroy it and re-create it from scratch), which is going to be much slower than a normal re-render. On top of that, this will lead to such bugs as:
Possible content flashing during re-renders
State reset in the component with every re-render
Loss of focus for focused components
useEffect with no dependencies triggered on every re-render
When a large component contains a small UI element that has its own state, move that state and UI logic into a separate, smaller component. A typical example would be opening/closing a modal with a button click in a complicated component that renders a significant portion of a page.
In this case, the state that controls modal dialog appearance, dialog itself, and the button that triggers the update can be encapsulated in a smaller component. This prevents the entire large component from re-rendering when only the small element needs to update.
This can also be called “wrap state around children”. This pattern is similar to “moving state down”: it encapsulates state changes in a smaller component. The difference here is that state is used on an element that wraps a slow portion of the render tree, so it can’t be extracted that easily.
A typical example would be onScroll or onMouseMove callbacks attached to the root element of a component. In this setup, we move the state handling and state-dependent parts into a smaller, focused component. The complex, slower-to-render content becomes this component's children. Since children is just another prop to the smaller component, it stays unaffected by state changes. This means the bulky content doesn't re-render when the state updates, keeping things smooth and efficient.
This pattern is similar to "children as props," but instead of passing complex content as children, we pass entire components as props to a state-managing wrapper. This approach isolates state changes in a smaller component, preventing unnecessary re-renders of the heavier components that are passed as props.
Use this pattern when you have multiple independent, heavy components that don't need to be aware of a particular state, but can't be grouped together as a single children prop.
When using Context in React, it's crucial to memoize the Provider's value if it's not at the app's root. This prevents unnecessary re-renders of all consumers when the Provider's parent component updates. By memoizing the value, you ensure that the context only triggers re-renders when the value actually changes, not on every render of the parent component.
When a Context contains both data and API (setters/getters), split them into separate Providers under the same component. This approach prevents unnecessary re-renders of components that only use the API, as they won't be affected by data changes. It's an effective way to optimize performance in applications with complex state management needs.
React.memo is a higher-order component that memoizes your component, preventing re-renders if its props haven't changed. For optimal performance, all non-primitive prop values (objects, arrays, functions) should be memoized as well, typically using useMemo or useCallback.
When using React.memo with components passed as props or children, apply it to these passed components, not the parent. Memoizing the parent won't prevent re-renders because children and props are objects that change on each render. Memoizing the passed components ensures they only re-render when their specific props change.
One of the use cases for useMemo is to avoid expensive calculations on every re-render. useMemo has its cost (consumes a bit of memory and makes initial render slightly slower), so it should not be used for every calculation. In React, mounting and updating components will be the most expensive calculation in most cases.
As a result, the typical use case for useMemo would be to memoize React elements. Usually parts of an existing render tree or results of generated render tree, like a map function that returns new elements.
In addition to the regular re-renders rules and patterns, the key attribute can affect the performance of lists in React.
Value in key should be a string, that is consistent between re-renders for every element in the list. Typically, item’s id or array’s index is used for that. It is okay to use array’s index as key, if the list is static, i.e. elements are not added/removed/inserted/re-ordered.
Randomly generated values should never be used as values in key
attribute in lists. They will lead to React re-mounting items on every re-render, which will lead to:
very poor performance of the list
bugs if items have state or any uncontrolled elements (like form inputs)
Join Sparsh on Peerlist!
Join amazing folks like Sparsh and thousands of other people in tech.
Create ProfileJoin with Sparsh’s personal invite link.
0
9
0