Don't re-render all FlatList items

December 16, 2022

Introduction

The FlatList component is often used in React Native apps for rendering lists. It's easy to display a simple list, but using data in an unsuitable structure can lead to unnecessary re-renders and performance issues.

In this blog post, we have an example app with this issue. I invite you to a step-by-step journey from discovering and investigating the problem to resolving it.

It's just a list

Let's imagine an app for collecting anonymous satisfaction surveys from restaurant customers. Users can rate their experiences by sliding multiple sliders. At the bottom, near the "Submit" button, there is an icon that shows the average rate based on your answers. It's updated as you slide any of the sliders.

Our awesome app to collect positive customer feedback

At the first glance, everything about the app looks great.

You can see the app code and tinker with it on Snack.

It's just a list, we said. We tested the app on a simulator, and it looked correct. What can go wrong? Let's publish it! 🚀

export const HomeScreen: React.FC = () => {
const { answers, updateAnswer } = useSurveyAnswers();
const renderItem: ListRenderItem<SurveyAnswer> = useCallback(
({ item: answer }) => {
const setValue = (newValue: number) => {
updateAnswer({ id: answer.id, value: newValue });
};
return <FormListItem answer={answer} setValue={setValue} />;
},
[]
);
return (
<SafeAreaView style={styles.container}>
<FlatList
contentContainerStyle={styles.contentContainer}
data={answers}
ListHeaderComponent={FormHeader}
renderItem={renderItem}
/>
<FormFooter style={styles.footer} />
</SafeAreaView>
);
};

So we did it. 🔥

As soon as the first users received the new app and started using it, the first negative reviews started coming. Some users were complaining that the app was slow when sliding.

Fortunately, we have a slow smartphone waiting specifically for such occasions. Quick testing on this device confirms there is a problem. On some devices, UI is freezing, lagging, and unresponsive.

What is going on?

We suspect the issue is somewhere near the list, so we start digging here.

<FlatList
contentContainerStyle={styles.contentContainer}
data={answers}
ListHeaderComponent={FormHeader}
renderItem={renderItem}
/>

FlatList has just 4 props passed in our app. Let's examine them closer to know if they can cause performance issues.

  • answers is a list of survey answers objects received from the state and is passed to the data prop of FlatList;
  • renderItem is wrapped with useCallback with no dependencies, so it will never change once set;
  • values of other props (contentContainerStyle and ListHeaderComponent) are defined once and never change.

We know that answers is the only prop that sometimes changes value. How is it affecting the app's behavior? Let's add two console.logs in src/pages/homeScreen.tsx to investigate:

export const HomeScreen: React.FC = () => {
// ...
useEffect(() => {
console.log("Data has changed");
}, [answers]);
const renderItem: ListRenderItem<SurveyAnswer> = useCallback(
({ item: answer }) => {
// ...
console.log("Rendered item: ", answer.question);
return <FormListItem answer={answer} setValue={setValue} />;
},
[]
);

Running the app, sliding a single slider, and previewing logs give a hint at what is wrong.

The user changes only a single item value, so intuition tells us that only this item should re-render. Instead, all list items re-render.

Slider change re-renders all

Each sliding step results in the whole list re-render.

Flamegraph chart

To further investigate why it is so, we can use React DevTools profiler. It's available in Flipper. Flamegraph tab brings special interest. It contains a timeline of React state changes. We can go step-by-step, and for each step, see a chart with clear indicators:

  • which component has rendered,
  • why the component has rendered,
  • and how long it took to render a particular component.

Green rectangles represent components that have rendered in this selected step. Gray rectangles represent components that have not rendered. Sadly, we don't see much gray here.

Profiling gives more insights

Everything green is re-rendering. It's bad.

Theory

React re-renders components when props or state change. By default, re-render propagates to all children components. Chart shows the FlatList component re-rendered because of the data prop change. It's where we pass answers.

Here is an example of how the answers array (data prop) looks like:

const answers = [
{
"id": "0",
"value": 8,
"question": "Wide selection in the menu"
},
{
"id": "1",
"value": 6,
"question": "Menu easy to read"
},
{
"id": "2",
"value": 9,
"question": "Order was taken promptly"
},
]

State in React is immutable, which means we can't update a single item in an array and use the same array again to render the next UI.

If we want to change the rate value in a single answer, let's say "Menu easy to read" from 6 to 7, we have to create a copy of the old list with a single item different. It results in a new answers object.

In console logs "Data has changed" represents answers value change, but we see it each time user updates just one of his answers. It's because whenever user slides one list item slider new answers list is passed as a new data prop value, which results in re-rendering the whole FlatList and all it's children.

However, there is a way to avoid those unnecessary re-renders and save CPU time. It will help slow smartphones to catch up with performance. 🐌

Let's give data the shape

What if we could change the shape of the data prop passed to FlatList to have the same value always?

Inside data, we can store just ids referring to "full" items. That would make it a string[] list, not a list of complex objects with a value field. When the user slides the slider and updates the value, it won't change the item id. The data list will stay unchanged.

This will be passed as a data prop:

const answersIdsList = [
"0",
"1",
"2",
]

Since now data stored in state has this shape:

{
dataIdsList: ["0", "1", "2"], // answersIdsList
dataById: {
"0": {
"value": 8,
"question": "Wide selection in the menu"
},
"1": {
"value": 6,
"question": "Menu easy to read"
},
// ...
};
}

To get the list of items without knowing too many details about item internals, we can use dataIdsList. This object won't be updated when the details of some items change, which won't cause re-rendering.

To get details of a single object, we can use dataById. For example, the updated list item component (NormalizedFormListItem) uses it to get item details while having only id as an input: state.dataById[id].

Old renderItem needed a full answer object as a param. It can be simplified:

// before fixes
const renderItem: ListRenderItem<SurveyAnswer> = useCallback(
({ item: answer }) => {
const setValue = (newValue: number) => {
updateAnswer({ id: answer.id, value: newValue });
};
return <FormListItem answer={answer} setValue={setValue} />;
},
[]
);

The new renderItem that supports normalized data now as a param needs only item id (answerId) to render complete NormalizedFormListItem. There is also no need for useCallback anymore because we can now move it out of the HomeScreen component:

const renderItem: ListRenderItem<string> = ({ item: answerId }) => (
<NormalizedFormListItem answerId={answerId} />;
);

Here is Snack with the updated app code.

Normalization

This concept of data shape is called normalization. Apart from performance, it improves data structure to eliminate data redundancy. You don't have to write the normalization boilerplate by yourself – libraries are helping with that, but for simplicity, we used no such library in this app.

If you want to know more about normalization and its advantages, we also have another blog post that goes deeper into this topic: Advanced Redux Patterns: Normalisation.

For podcast fans, in the BBS 12: Normalising app state episode, Wojciech and Łukasz discussed benefits, implementation tips, and potential pitfalls of normalizing app state in your React Native mobile app.

Is it fixed?

We can add console.logs as we did before to see data changing or list items re-rendering.

Slider change re-renders single item

Finally, only the changed item is re-rendered

Note that:

  • "Rerendered item" log is only shown for a single item that was just updated – not for all items as before,
  • "Data has changed" log is not shown anymore because data is now a list of ids, and it's not changed when the user slides the value of any item.

What does it look like in DevTools Flamegraph?

DevTools show only single item re-rendering

Gray here looks much better

Most rectangles are gray, which means those components didn't re-render. If we compare the total rendering duration, it's re-rendering on single list items updates in approximately 10x less time.

That's a huge improvement. We can call it a success! 🎉

Summary

Usually, re-rendering is not an issue in React. It becomes a problem only if components are re-rendering unnecessarily and excessively. A minor state update can result in an avalanche, so it's good to test on slow low-end smartphones. They do not forgive performance issues that are easy to miss on modern fast smartphones.

Having the issue in the app, we debugged and profiled it to find the performance issue source. Then we fixed it by normalizing the list data state. We were also able to measure improvement and confirm resolving the issue.

Lesson learned

Now you are ready to answer those questions:

  • Why FlatList is sometimes re-rendering all list items if just a single item changed?
  • How to investigate what's the cause of component re-rendering?
  • How to re-render only a single FlatList item instead of all when it's updated?

Blog post updates:

January 10, 2023: Added a link to our new podcast episode about normalization.

Want more?

If you liked this post, why don't you subscribe for more content? If you're as old-school as we are, you can just grab the RSS feed of this blog. Or enroll to the course described below!

Alternatively, if audio's more your thing why don't you subscribe to our podcast! We're still figuring out what it's going to be, but already quite a few episodes are waiting for you to check them out.

Blog author: Szymon Koper
WRITTEN BY

Szymon Koper

React Native developer

Happy puzzle phone

More Brains and Beards stories