AniUI Academy

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.

9 min read

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.

Try it yourself
Loading playground...

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 sections prop is an array of { title, data } objects — the grouping already has to exist before SectionList sees it.
  • renderItem renders each row; renderSectionHeader renders the label above each section's rows — two separate props, two separate jobs.
  • Pull-to-refresh is refreshing (a boolean you own) plus onRefresh (a callback you implement), the same pair across FlatList, SectionList, and ScrollView.
  • Forgetting to flip refreshing back to false leaves 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

up to 50
  1. 1.What shape does SectionList's sections prop expect?

  2. 2.What's the difference between renderItem and renderSectionHeader on a SectionList?

  3. 3.Which pair of props implements pull-to-refresh on a FlatList, SectionList, or ScrollView?

  4. 4.Why write groupByFirstLetter as a plain function, separate from the SectionList that will render its output?

4 left to answer