Alex Sidorenko AvatarAlex Sidorenko

12 Jul 2021

How to stop re-rendering lists in React?

You have a list of components in React. The parent holds the state and passes it to the list items. Every time you update the property of one of the components in the list, the entire list re-renders. How can you prevent that?

Components always re-render

First, let's simplify our example by removing all props from the Item. We'll still update the parent state but won't pass any props to list items.

There's a common misconception that a React component won't re-render unless one of its properties changes. This isn't true:

React does not care whether "props changed" - it will render child components unconditionally just because the parent rendered!

โ€” Mark Erikson, A (Mostly) Complete Guide to React Rendering Behavior

If you don't want a component to re-render when its parent renders, wrap it with memo. After that, the component will indeed only re-render when its props change.

const Item = memo(() => <div>Item</div>);

Applying memo to our problem

Let's return to our initial example and wrap Item with memo. Here's a slightly simplified code.

const Item = memo(({ id, value, onChange }) => {
return <input onChange={(e) => onChange(id, e.target.value)} value={value} />;
});

It doesn't work. We still have the same problem. But why?

If a component wrapped with memo re-renders, it means that one of its properties has changed. Let's figure out which one.

Memoizing properties

We know from looking at the state that value only changes for one item in the list. The id property is also stable. So it must be the onChange property that's changing. Let's check the Parent code to see how we pass the props.

const Parent = () => {
const [items, setItems] = useState([
{ value: "" },
{ value: "" },
{ value: "" },
]);
return (
<div>
{items.map((item, index) => (
<Item
key={index}
id={index}
value={item.value}
onChange={(id, value) =>
setState(
state.map((item, index) => {
return index !== id ? item : { value: value };
})
)
}
/>
))}
</div>
);
};

Here's our problem:

setState(
state.map((item, index) => {
return index !== id ? item : { value: value };
})
);

Anonymous functions will always get a new reference on every render. This means that the onChange property will change every time Parent renders. To prevent that, we need to memoize it with useCallback. Let's do that:

const Parent = () => {
...

const onChange = useCallback((id, value) => {
setItems(items.map((item, index) => {
return index !== id ? item : { value: value }
}))
}, [items])

return (
<div>
{items.map((item, index) => (
<Item
key={index}
id={index}
value={item.value}
onChange={onChange}
/>
)}
</div>
)
}

It still doesn't work - every component re-renders.

This happens because we put items as a dependency for useCallback. Every time items updates, useCallback returns a new reference of the function. This causes the onChange prop to change, therefore updating every component in the list.

To fix this, we need to stop relying on items as a dependency. We can achieve that with a functional state update:

const onChange = useCallback((id, value) => {
setItems((prevItems) =>
prevItems.map((item, index) => {
return index !== id ? item : { value: value };
})
);
}, []); // No dependencies

Now, the only property of the Item that changes is value. And since we only update one value at a time, it prevents other components in the list from re-rendering.

Should I do that for every list?

You don't have to optimize every unnecessary re-render in React. React render is quite performant. It only updates DOM when needed. And memo comes with a small performance cost as well. Optimize it when you have a lot of items in the list and your render function is expensive.

I would assume that the same general advice applies for React.memo as it does for shouldComponentUpdate and PureComponent: doing comparisons does have a small cost, and there's scenarios where a component would never memoize properly (especially if it makes use of props.children). So, don't just automatically wrap everything everywhere. See how your app behaves in production mode, use React's profiling builds and the DevTools profiler to see where bottlenecks are, and strategically use these tools to optimize parts of the component tree that will actually benefit from these optimizations.

โ€” Mark Erikson - When should you NOT use React memo?


Follow me on ๐• for short videos about Next.js

๐• Follow for more