Lesson 13 of 26
SectionList and Pull-to-Refresh
SectionList's grouped-data shape and header rendering, RefreshControl for pull-to-refresh, and the plain function that shapes flat data into sections.
You've seen FlatList for a single flat list of rows. SectionList is for
the very common case where that list actually has structure — contacts
grouped by first letter, orders grouped by date, settings grouped by
category. Rather than reinventing that grouping by hand inside one giant
FlatList, SectionList bakes "grouped data with headers" in as a first-class
concept.
The sections shape
SectionList's sections prop is an array of section objects, and each one
carries its own data array:
const sections = [
{ title: "A", data: [{ id: "1", name: "Ada" }, { id: "2", name: "Amir" }] },
{ title: "B", data: [{ id: "3", name: "Bo" }] },
];Rendering it looks close to FlatList, with one addition:
import { SectionList, Text, View } from "react-native";
function ContactList({ sections }: { sections: Section[] }) {
return (
<SectionList
sections={sections}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={{ padding: 12 }}>
<Text>{item.name}</Text>
</View>
)}
renderSectionHeader={({ section }) => (
<View style={{ padding: 8, backgroundColor: "#eee" }}>
<Text style={{ fontWeight: "600" }}>{section.title}</Text>
</View>
)}
/>
);
}renderItem is called once per item inside a section's data, the same
job it does for FlatList. renderSectionHeader is the new piece — it's
called once per section, and receives that section (so section.title is
available), to render the sticky-feeling label above its rows. Everything
you already know about FlatList performance — keyExtractor, why it
matters, virtualization happening under the hood — still applies here;
SectionList is built on the same underlying machinery, just aware of the
extra section boundary.
Pull-to-refresh
Pull-to-refresh — dragging a scrollable list down past its top to trigger a
reload — is implemented with the same pair of props across FlatList,
SectionList, and plain ScrollView: refreshing and onRefresh.
function ContactList({ sections, onReload }: Props) {
const [refreshing, setRefreshing] = useState(false);
const handleRefresh = async () => {
setRefreshing(true);
await onReload();
setRefreshing(false);
};
return (
<SectionList
sections={sections}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <ContactRow contact={item} />}
renderSectionHeader={({ section }) => <SectionHeader title={section.title} />}
refreshing={refreshing}
onRefresh={handleRefresh}
/>
);
}refreshing is the boolean you own — it tells the list whether to display
its built-in refresh spinner right now. onRefresh is the callback the
list invokes when the user performs the pull gesture; it's on you to kick
off whatever refetch that implies and flip refreshing back to false
once it resolves. Skip the flip and the spinner will spin forever, looking
like the refresh never finished even after new data has already arrived.
Under the hood this is powered by a component called RefreshControl,
which FlatList/SectionList/ScrollView all accept directly as a
refreshControl prop too — useful if you want to customize the spinner's
tint color or title, but for the common case, the plain refreshing/
onRefresh pair on the list itself is all you need.
The data-shaping step is plain logic
Getting a flat array into the { title, data } shape SectionList expects
is a transform, and like the form-validation logic from the TextInput
lesson, it doesn't need React Native to exist. Here it is grouping contacts
by first letter:
function groupByFirstLetter(
items: { name: string }[]
): { title: string; data: { name: string }[] }[] {
const groups: Record<string, { name: string }[]> = {};
for (const item of items) {
const letter = item.name.charAt(0).toUpperCase();
groups[letter] ??= [];
groups[letter].push(item);
}
return Object.keys(groups)
.sort()
.map((title) => ({ title, data: groups[title] }));
}Feed it a flat, unsorted array of names and it hands back exactly the
sections shape from earlier — sorted, grouped, ready to pass straight into
sections.
The data-shaping step here is plain array logic, identical on any platform;
only the rendering component — SectionList — is React Native-specific.
That split is worth keeping deliberate: groupByFirstLetter is easy to
unit test with a handful of made-up names and no simulator running, and it
would produce the exact same output if this were a web app grouping the
same contacts into an HTML list instead.
What to remember
- SectionList's
sectionsprop is an array of{ title, data }objects — the grouping already has to exist before SectionList sees it. renderItemrenders each row;renderSectionHeaderrenders the label above each section's rows — two separate props, two separate jobs.- Pull-to-refresh is
refreshing(a boolean you own) plusonRefresh(a callback you implement), the same pair across FlatList, SectionList, and ScrollView. - Forgetting to flip
refreshingback tofalseleaves the spinner stuck even after data has reloaded. - Turning flat data into the sections shape is ordinary array logic — write and test it as a plain function, independent of the component that eventually renders it.
Check yourself
4 questions · pass 3/4 to unlock Navigation Concepts: Stack, Tab, and Drawer
1.What shape does SectionList's sections prop expect?
2.What's the difference between renderItem and renderSectionHeader on a SectionList?
3.Which pair of props implements pull-to-refresh on a FlatList, SectionList, or ScrollView?
4.Why write groupByFirstLetter as a plain function, separate from the SectionList that will render its output?
4 left to answer