Rendering Performant Cross-Platform Lists in React Native and Expo
In a dynamic mobile app development landscape, efficiently rendering lists is paramount for delivering a smooth user experience. React Native provides two powerful components, FlatList and SectionList, or Shopify’s FlashList to tackle this challenge. In this guide, we’ll explore these components, delve into best practices, and learn how to optimize performance using PureComponent.
Why not ScrollView?
ScrollView is one of the most fundamental components in React Native for creating scrollable content. It’s a versatile component that allows you to render a collection of child components, whether text, images, or custom views, and enables users to scroll through them vertically or horizontally.
Here’s a simple example of how to use ScrollView
:
import React from 'react';
import { ScrollView, Text, StyleSheet } from 'react-native';
const MyScrollView = () => {
return (
<ScrollView style={styles.container}>
<Text style={styles.item}>Item 1</Text>
<Text style={styles.item}>Item 2</Text>
<Text style={styles.item}>Item 3</Text>
{/* Add more items as needed */}
</ScrollView>
);
};
export default MyScrollView;
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
item: {
fontSize: 18,
padding: 10,
backgroundColor: '#f9f9f9',
marginBottom: 10,
borderRadius: 5,
},
});
In this example, ScrollView
renders a list of text items that users can scroll through. It’s straightforward to use and works well for small, static lists.
The Downsides of ScrollView
While ScrollView
is great for simple use cases, and has significant limitations when dealing with large datasets or dynamic content. Here’s why:
- Renders All Items at Once:
ScrollView
renders all its children immediately, regardless of whether they are visible on the screen. For example, if you have a list of 1,000 items,ScrollView
will attempt to render all 1,000 items at once. This can lead to:
- Performance bottlenecks: Rendering too many items can cause lag, especially on lower-end devices.
- Increased memory usage: Keeping all items in memory can strain the device, leading to crashes or slow performance. - No Item Recycling: Unlike
FlatList
orSectionList
,ScrollView
does not reuse list items as the user scrolls. This means every item is created and maintained in memory, even if it’s not visible on the screen. - Not Scalable: As your app grows and your datasets become larger, ScrollView becomes increasingly impractical. It’s simply not designed to handle long lists efficiently.
Flattening the Field with FlatList
FlatList is one of the most powerful and commonly used components in React Native for rendering large, dynamic lists. Unlike ScrollView which renders all items at once, FlatList employs virtualization. This means it only renders the currently visible on the screen, recycling components as the user scrolls. This approach makes FlatList
highly performant, even for lists with thousands of items.
Why Use FlatList?
- Efficient Rendering: Only visible items are rendered, reducing memory usage and improving performance.
- Dynamic Data Handling: Perfect for lists where data might change or load dynamically (e.g., paginated data).
- Built-in Features: Comes with handy features like pull-to-refresh, lazy loading, and scroll-to-index.
Basic Usage of FlatList
To use FlatList
, you need to provide three key props:
data
: An array of items to render.renderItem
: A function that defines how each item is displayed.keyExtractor
: A function that generates a unique key for each item (helps React Native optimize re-renders).
Here’s a simple example:
import React from 'react';
import { FlatList, Text, StyleSheet, View } from 'react-native';
const MyFlatList = () => {
const data = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
// ... more items
];
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.text}>{item.name}</Text>
</View>
);
return (
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
/>
);
};
export default MyFlatList;
const styles = StyleSheet.create({
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
},
text: {
fontSize: 16,
},
});
In this example:
- The
data
prop contains an array of objects, each representing a list item. - The
renderItem
function defines how each item is displayed (in this case, a simpleText
component inside aView
). - The
keyExtractor
ensures each item has a unique key, essential for React Native to manage list updates efficiently.
Advanced Features of FlatList
FlatList
comes with several advanced features that make it even more powerful:
onEndReached
: Trigger a function when the user scrolls to the end of the list. This is useful for implementing infinite scrolling or pagination.
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
onEndReached={() => console.log('End of list reached!')}
onEndReachedThreshold={0.5} // Trigger when 50% of the last item is visible
/>
2. ListHeaderComponent
and ListFooterComponent
: Add headers or footers to your list.
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
ListHeaderComponent={<Text style={styles.header}>My List Header</Text>}
ListFooterComponent={<Text style={styles.footer}>My List Footer</Text>}
/>
3. horizontal
: Render a horizontal list instead of a vertical one.
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
horizontal
/>
4. ItemSeparatorComponent
: Add a separator between list items.
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
When to Use FlatList
Use FlatList
when:
- You have a large dataset that needs to be rendered efficiently.
- Your data is dynamic (e.g., fetched from an API or updated frequently).
- You need advanced features like pull-to-refresh, infinite scrolling, or custom headers/footers.
Pro Tips for Optimizing FlatList
- Keep
renderItem
Light: Avoid heavy computations or deeply nested components in yourrenderItem
function. UseReact.memo
to prevent unnecessary re-renders.
const MemoizedItem = React.memo(({ item }) => (
<View style={styles.item}>
<Text style={styles.text}>{item.name}</Text>
</View>
));
- Use
getItemLayout
for Fixed-Height Items: If your list items have a fixed height, provide thegetItemLayout
prop to skip measurement and improve performance.
<FlatList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
getItemLayout={(data, index) => (
{ length: 50, offset: 50 * index, index }
)}
/>
- Optimize
keyExtractor
: EnsurekeyExtractor
generates unique and stable keys for each item. Avoid using theindex
as the key unless necessary.
Sectioning Things Up with SectionList
While FlatList is perfect for rendering homogeneous lists, SectionList is the ideal choice when your data has a hierarchical or categorized structure.
Think of an address book with sections for each letter of the alphabet, or a menu with categories like "Appetizers", "Main Course", and "Desserts". SectionList allows you to group your data into sections, each with its header and items.
Like FlatList, SectionList uses virtualization to render only the visible items, ensuring high performance even with large datasets. However, it adds the ability to organize data into sections, making it a powerful tool for structured lists.
Basic Usage of SectionList
To use SectionList, you need to provide the following key props:
sections
: An array of objects, where each object represents a section. Each section object must have adata
property (an array of items) and can optionally include atitle
or other metadata.renderItem
: A function that defines how each item within a section is displayed.renderSectionHeader
: A function that defines how each section header is displayed (optional but highly recommended).
Here’s a simple example:
import React from 'react';
import { SectionList, Text, StyleSheet, View } from 'react-native';
const MySectionList = () => {
const sections = [
{
title: 'A',
data: ['Alice', 'Andrew', 'Anna'],
},
{
title: 'B',
data: ['Bob', 'Brian'],
},
{
title: 'C',
data: ['Charlie', 'Chris', 'Catherine'],
},
];
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.text}>{item}</Text>
</View>
);
const renderSectionHeader = ({ section }) => (
<View style={styles.sectionHeader}>
<Text style={styles.sectionHeaderText}>{section.title}</Text>
</View>
);
return (
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={(item, index) => item + index}
/>
);
};
export default MySectionList;
const styles = StyleSheet.create({
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
},
text: {
fontSize: 16,
},
sectionHeader: {
backgroundColor: '#f9f9f9',
padding: 10,
},
sectionHeaderText: {
fontSize: 18,
fontWeight: 'bold',
},
});
In this example:
- The
sections
prop contains an array of section objects, each with atitle
anddata
property. - The
renderItem
function defines how each item within a section is displayed. - The
renderSectionHeader
function defines how each section header is displayed.
Advantages of SectionList
- Structured Data:
SectionList
is perfect for organizing data into meaningful groups, making it easier for users to navigate and understand. - Performance: Like
FlatList
, it uses virtualization to render only visible items, ensuring smooth performance even with large datasets. - Customizable Headers: You can fully customize section headers to match your app’s design and functionality.
Advanced Features of SectionList
SectionList shares many of the same advanced features as FlatList, but with additional capabilities for handling sections:
SectionSeparatorComponent
: Add a separator between sections.
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
SectionSeparatorComponent={() => <View style={styles.sectionSeparator} />}
/>
stickySectionHeadersEnabled
: Make section headers stick to the top of the screen as the user scrolls.
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
stickySectionHeadersEnabled
/>
ListHeaderComponent
andListFooterComponent
: Add a header or footer
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
ListHeaderComponent={<Text style={styles.header}>My Address Book</Text>}
ListFooterComponent={<Text style={styles.footer}>End of List</Text>}
/>
onEndReached
: Trigger a function when the user scrolls to the end of the list (useful for pagination or infinite scrolling).
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
onEndReached={() => console.log('End of list reached!')}
onEndReachedThreshold={0.5}
/>
When to Use SectionList
Use SectionList
when:
- Your data is naturally grouped into sections (e.g., categorized menus, address books, or timelines).
- You need to display section headers that provide context or navigation.
- You want to leverage sticky headers for a better user experience.
Pro Tips for Optimizing SectionList
- Keep Section Data Lean: Avoid nesting too much data within each section. Keep your section objects simple and focused.
- Memoize Components: Use
React.memo
forrenderItem
andrenderSectionHeader
to prevent unnecessary re-renders.
const MemoizedItem = React.memo(({ item }) => (
<View style={styles.item}>
<Text style={styles.text}>{item}</Text>
</View>
));
const MemoizedHeader = React.memo(({ section }) => (
<View style={styles.sectionHeader}>
<Text style={styles.sectionHeaderText}>{section.title}</Text>
</View>
));
- Use
keyExtractor
Wisely: EnsurekeyExtractor
generates unique and stable keys for each item same as FlatList. Avoid using theindex
as the key unless necessary.
Optimizing for a Flawless Performance
Here are some extra tips to squeeze the most performance out of your FlatList and SectionList:
- Data Diet: Keep your data objects lean and mean. Avoid unnecessary nesting, like a treasure chest inside another treasure chest (it gets confusing!).
- Memoization Magic: If your item renderer involves complex calculations, use
React.memo
to prevent unnecessary re-renders. - Initial Number Insight: Give FlatList or SectionList a heads-up about the initial number of items to render. This helps it avoid scrambling to display everything at once.
Be More Performant with FlashList
While FlatList
and SectionList
are powerful tools for rendering lists in React Native, FlashList takes performance to the next level. Built by Shopify, FlashList is designed to easily handle large datasets and complex list items, offering smoother scrolling, faster rendering, and better memory management. It’s a drop-in replacement for FlatList
but with significant optimizations under the hood.
Why FlashList?
FlashList was created to address the limitations of FlatList
and SectionList
in handling highly dynamic and complex lists. Here’s why it stands out:
- Faster Rendering: FlashList uses a more efficient rendering mechanism, reducing the time it takes to display items on the screen.
- Memory Efficiency: It recycles list items more effectively, reducing memory usage and preventing crashes on low-end devices.
- Smooth Scrolling: FlashList ensures buttery-smooth scrolling, even with complex list items or large datasets.
- Advanced Features: It introduces features like estimated item size, horizontal layouts, and sticky headers.
Basic Usage of FlashList
Using FlashList is as simple as replacing FlatList with FlashList. Here’s a basic example:
First, install the library into your project
npx expo install @shopify/flash-list
Then use it as below:
import React from 'react';
import { Text, View, StyleSheet } from 'react-native';
import { FlashList } from '@shopify/flash-list';
const MyFlashList = () => {
const data = [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' },
{ id: 3, name: 'Item 3' },
// ... more items
];
const renderItem = ({ item }) => (
<View style={styles.item}>
<Text style={styles.text}>{item.name}</Text>
</View>
);
return (
<FlashList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
estimatedItemSize={100} // Provide an estimate for smoother performance
/>
);
};
export default MyFlashList;
const styles = StyleSheet.create({
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#ccc',
},
text: {
fontSize: 16,
},
});
In this example:
- The
data
prop contains an array of items. - The
renderItem
function defines how each item is displayed. - The
estimatedItemSize
prop helps FlashList calculate layout and scroll position more accurately.
Advanced Features of FlashList
FlashList comes packed with advanced features that make it a superior choice for complex scenarios:
Estimated Item Size
FlashList requires an estimatedItemSize
prop to optimize performance. This helps the list calculate layout and scroll position without measuring every item.
<FlashList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
estimatedItemSize={200} // Provide an estimate for smoother performance
/>
Horizontal Lists
FlashList supports horizontal scrolling out of the box, making it perfect for carousels or grids.
<FlashList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
horizontal
/>
Sticky Headers
FlashList supports sticky headers, which are useful for categorized lists.
<FlashList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
stickyHeaderIndices={[0, 5, 10]} // Indices of sticky headers
/>
Pull-to-Refresh
FlashList supports pull-to-refresh functionality, just like FlatList
.
const [refreshing, setRefreshing] = React.useState(false);
const onRefresh = () => {
setRefreshing(true);
// Fetch new data
setRefreshing(false);
};
<FlashList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
refreshing={refreshing}
onRefresh={onRefresh}
/>
Viewability Tracking
FlashList allows you to track which items are visible on the screen, which is useful for analytics or lazy loading.
const onViewableItemsChanged = ({ viewableItems }) => {
console.log('Visible items:', viewableItems);
};
<FlashList
data={data}
renderItem={renderItem}
keyExtractor={(item) => item.id.toString()}
onViewableItemsChanged={onViewableItemsChanged}
/>
Complex Scenarios Where FlashList Shines
1. Large Datasets with Complex Items
FlashList excels at rendering large datasets with complex list items, such as product listings with images, ratings, and descriptions. Its efficient recycling mechanism ensures smooth scrolling and low memory usage.
2. Nested Lists
If your app requires nested lists (e.g., a list of categories with subcategories), FlashList’s performance optimizations make it a better choice than FlatList
or SectionList
.
3. Real-Time Updates
For apps that require real-time updates (e.g., chat apps or live feeds), FlashList’s efficient rendering ensures that new items are added smoothly without jank.
4. Custom Layouts
FlashList supports custom layouts, making it ideal for grids, masonry layouts, or other non-standard list designs.
When to Use FlashList
Use FlashList when:
- You’re dealing with large datasets or complex list items.
- You need smooth scrolling and high performance.
- Your app requires real-time updates or custom layouts.
- You want to leverage advanced features like sticky headers or viewability tracking.
Choosing the Right Tool for the Job
As a React Native developer, your choice of list component depends on the specific requirements of your app. Here’s a quick guide to help you decide:
- ScrollView: Use it for small, static lists where performance is not a concern. Avoid it for large datasets.
- FlatList: Ideal for most use cases, especially when dealing with long, homogeneous lists. It’s performant, easy to use, and highly customizable.
- SectionList: Perfect for hierarchical or categorized data, such as contact lists or menus with sections.
- FlashList: When performance is critical, and you’re dealing with large datasets or complex list items, FlashList is the way to go.
Recommended Best Practices
As you grow in your React Native journey, keep these principles in mind:
- Understand the Basics First: Before diving into advanced tools like FlashList, ensure you have a solid understanding of core components like
FlatList
andSectionList
. This foundation will help you appreciate the optimizations that FlashList brings to the table. - Profile and Optimize: Always profile your app’s performance using tools like React Native Debugger, or the built-in performance monitor. Identify bottlenecks and optimize accordingly.
- Keep Your Components Light: Whether you’re using
FlatList
,SectionList
, orFlashList
, ensure your list items are lightweight. Avoid heavy computations or deeply nested components in your render functions. - Leverage Memoization: Use
React.memo
to prevent unnecessary re-renders of list items. This is especially useful when your list items depend on complex props or states. - Stay Updated: The React Native ecosystem evolves rapidly. Keep an eye on new libraries, tools, and best practices.
- Experiment and Learn: Don’t be afraid to experiment with different list components and optimization techniques. The more you practice, the better you’ll understand how to build performant apps.
Performance is not an afterthought; it’s a mindset.
Every decision you make — from choosing the right component to optimizing your code — should be guided by the goal of delivering a seamless user experience. Remember, your users don’t care about the complexity of your code; they care about how fast and smooth your app feels.
By mastering tools like FlatList
, SectionList
, and FlashList
, and adopting best practices, you’ll be well on your way to building high-performance React Native apps that delight users.
Happy coding!