Basics
Re-Rendering​
- Glossary
- Reasons
- Prevention
- initial render - happens when a component first appears on the screen
- re-render - second and any consecutive render of a component that is already on the screen. Re-renders in React happen when the app updates with new data, usually from user interactions or asynchronous requests
- necessary re-render - updates a component directly reflecting changes (e.g., input field updates on keystrokes)
- unnecessary re-render - caused by inefficiencies, leading to broader re-renders (e.g., entire page re-renders on keystrokes). Excessive re-renders can cause lag, delays, or unresponsiveness
- State
- Parent
- Context
- Hooks
- Props (myth)
when a component's state changes, it re-renders, typically in a callback or the useEffect
hook. State changes are the primary cause of re-renders
const Component = () => {
const [state, setState] = useState(); // 1. state changed
return (...);
} // 2. re-render whole component
re-rendering flows down the tree parent → children but not vice versa
const Parent = () => { // 1. re-renders
return <Child /> // 2. re-renders children
}
when the value in a Context Provider changes, all components using that Context will re-render, even if they don't directly use the changed data (cascading). Memoization can't prevent these re-renders
// useUser.js
const useUser = useContext(UserContext) => { // 1. re-renders
}
// UserProfile.js
const userProfile = () => {
const user = useUser();
return (...);
}
// UserDetails.js
const UserDetails = () => {
const user = useUser();
return (...);
} // 2. both will be re-rendered (UserProfile and UserDetails)
all actions within a hook are tied to the component that uses it. The same rules for Context and State changes apply:
- state change within the hook will cause an unavoidable re-render of the host component
- if the hook uses Context and its value changes, it will also trigger an unavoidable re-render of the host component
Hooks can be chained, and each hook in the chain remains associated with the host component, following the same rules
// useUser.js
const useUser = useContext(UserContext) => { // 1. value changes
const useName = { // 2. chain reaction
useName()
}
}
// UserComponent.js
const UserComponent = () => { // 3. re-renders
const name = useName();
return (...);
}
For non-memoized components, it doesn't matter if the component's props change (they are not the sole reason for re-renders); if the parent re-renders, the child will re-render as well. Props can only change if the parent updates them, which triggers the child's re-render. However, when using memoization techniques like React.memo
or useMemo
, prop changes become significant
const Parent = () => { // 1. re-renders
return <Child value={{value}} />;
} // 2. doesn't matter of props change children will re-render
- Composition
- React.memo
- useMemo/useCallback
- Context
- Performance of Lists
Anti-pattern: Creating components in render function​
Creating components inside the render function of another component is an anti-pattern that can severely impact performance. Each re-render causes React to re-mount the inner component, which is slower than a normal re-render.
This can lead to issues such as:
- content "flashes" during re-renders
- state being reset with each re-render
useEffect
without dependencies triggering on every re-render- loss of focus if the component was focused
Case | Solution |
---|---|
|
|
Moving state down​
Useful when a heavy component manages state that only affects a small, isolated part of the render tree. For example, consider a complex component that opens or closes a dialog with a button click
Case | Solution |
---|---|
|
|
Children as props​
Known as "wrapping state around children," is similar to "moving state down" but focuses on encapsulating state changes in a smaller component that wraps a slower part of the render tree. A common example is using onScroll
or onMouseMove
callbacks on the root element of a component
Case | Solution |
---|---|
|
|
Components as props​
Encapsulates state within a smaller component while passing heavy components as props. Since props are not affected by state changes, the heavy components won't re-render. Useful when several heavy components are independent of the state but cannot be extracted as a group of children
Case | Solution |
---|---|
|
|
React.memo​
Wrapping a component in React.memo
will prevent downstream re-renders triggered by changes higher up in the render tree, unless the component's props have changed. Useful for rendering heavy components that do not depend on the source of re-renders, such as state or updated data.
Case | Solution |
---|---|
|
|
Component with props​
For React.memo
to work effectively, all props that are not primitive values must be memoized
Case | Solution |
---|---|
|
|
Components as props or children​
React.memo
must be applied to the elements passed as children or props. Simply memoizing the parent component won't suffice, as children and props are typically objects that change with every re-render
Case | Solution |
---|---|
|
|
Necessary useMemo/useCallback​
Memoizing props alone won't prevent a child component from re-rendering. If the parent component re-renders, it will trigger a re-render of the child component, regardless of the props
If a child component is wrapped in React.memo
, all non-primitive props must be memoized to prevent unnecessary re-renders. If a component uses a non-primitive value as a dependency in hooks like useEffect
, useMemo
, or useCallback
, that value should be memoized to avoid unnecessary re-renders or recalculations
Case | Solution |
---|---|
|
|
useMemo for expensive calculations​
useMemo
has its own cost, consuming some memory and slightly slowing down the initial render, so it shouldn't be used for every calculation. In most cases, mounting and updating components are the most expensive operations, unless you're performing complex calculations like generating prime numbers, which should generally be avoided on the frontend. Typically, useMemo
is best used to memoize React elements, such as parts of an existing render tree or results from a map function that generates new elements. The cost of "pure" JavaScript operations like sorting or filtering an array is usually negligible compared to component updates
Case | Solution |
---|---|
|
|
Memoizing Provider value​
If a Context Provider is not placed at the very root of the app and may re-render due to changes in its ancestor components, its value should be memoized. This helps prevent unnecessary re-renders of components that consume the context
Case | Solution |
---|---|
|
|
Splitting data and API​
If a Context contains both data and API (getters and setters), they can be split into separate Providers within the same component. This way, components that only use the API won't re-render when the data changes
Case | Solution |
---|---|
|
|
Splitting data into chunks​
If a Context manages multiple independent data chunks, they can be split into smaller providers under the same parent provider. This approach ensures that only the consumers of the changed data chunk will re-render
Case | Solution |
---|---|
|
|
Context selectors​
There is no way to prevent a component that uses a portion of a Context value from re-rendering, even if that specific piece of data hasn't changed, and even with the useMemo
hook. However, Context selectors can be simulated using higher-order components and React.memo
to optimize re-renders
Case | Solution |
---|---|
|
|
Anti-pattern: random value as key in lists​
Simply providing a key
attribute won't improve performance; to prevent re-renders of list elements, you need to wrap them in React.memo
and follow its best practices. The key
value should be a consistent string for each element in the list across re-renders, typically using an item's ID or the array's index. Using the array's index as a key is acceptable for static lists where elements are not added, removed, or reordered
Randomly generated values or index as a key in dynamic lists should never be used as the key attribute in lists. Doing so will cause React to re-mount items on every re-render, resulting in:
- bugs if items have state or uncontrolled elements (like form inputs)
- degraded performance if items are wrapped in
React.memo
Case | Solution |
---|---|
|
|