Module 5: Lists and Performance
SectionList for Grouped Data
Organize your content with section headers and grouped items
π― Learning Objectives
- Understand when to use SectionList vs FlatList
- Structure data for SectionList consumption
- Implement section headers and item rendering
- Create sticky section headers for better UX
- Use section and item separators effectively
- Build common patterns: contacts, settings, grouped content
- Apply TypeScript for type-safe sectioned data
When to Use SectionList
SectionList is FlatList's sibling, designed specifically for rendering grouped data with section headers. While you could build this with FlatList, SectionList makes it much cleaner.
FlatList vs SectionList
π‘ Choose SectionList When...
- Data is naturally grouped (contacts by letter, settings by category)
- You need section headers that describe groups
- You want sticky headers as users scroll
- Different sections might have different item types
Choose FlatList When...
- Data is a simple flat array
- No grouping or headers needed
- You're doing your own grouping logic
- Performance is critical (SectionList has slightly more overhead)
Common Use Cases
flowchart LR
subgraph SectionList["SectionList Use Cases"]
S1["π± Contacts
(A-Z groups)"]
S2["βοΈ Settings
(categories)"]
S3["π
Events
(by date)"]
S4["π Products
(by category)"]
S5["π§ Inbox
(Today, Yesterday)"]
end
style S1 fill:#e3f2fd
style S2 fill:#e3f2fd
style S3 fill:#e3f2fd
style S4 fill:#e3f2fd
style S5 fill:#e3f2fd
The Section Data Structure
SectionList requires a specific data structure: an array of section objects, each containing a data array for its items.
Required Structure
// The sections prop expects this shape
const sections = [
{
// Optional: any extra data for the section
title: 'Section Title',
// Required: array of items in this section
data: [item1, item2, item3],
},
{
title: 'Another Section',
data: [item4, item5],
},
];
// Minimal example
const minimalSections = [
{ data: ['A', 'B', 'C'] },
{ data: ['D', 'E'] },
];
Transforming Flat Data to Sections
// Starting with flat data
interface Contact {
id: string;
name: string;
phone: string;
}
const flatContacts: Contact[] = [
{ id: '1', name: 'Alice Smith', phone: '555-0001' },
{ id: '2', name: 'Bob Johnson', phone: '555-0002' },
{ id: '3', name: 'Amy Brown', phone: '555-0003' },
{ id: '4', name: 'Brian Davis', phone: '555-0004' },
];
// Transform to sections (grouped by first letter)
interface Section {
title: string;
data: Contact[];
}
const groupByFirstLetter = (contacts: Contact[]): Section[] => {
// Group contacts by first letter
const groups = contacts.reduce((acc, contact) => {
const letter = contact.name[0].toUpperCase();
if (!acc[letter]) {
acc[letter] = [];
}
acc[letter].push(contact);
return acc;
}, {} as Record<string, Contact[]>);
// Convert to section array and sort
return Object.entries(groups)
.sort(([a], [b]) => a.localeCompare(b))
.map(([letter, contacts]) => ({
title: letter,
data: contacts.sort((a, b) => a.name.localeCompare(b.name)),
}));
};
const sections = groupByFirstLetter(flatContacts);
// Result:
// [
// { title: 'A', data: [Alice, Amy] },
// { title: 'B', data: [Bob, Brian] },
// ]
Adding Section Metadata
// Sections can include any extra properties
interface CategorySection {
title: string;
subtitle?: string;
icon?: string;
collapsible?: boolean;
data: Product[];
}
const productSections: CategorySection[] = [
{
title: 'Electronics',
subtitle: '24 items',
icon: 'π±',
data: electronicsProducts,
},
{
title: 'Clothing',
subtitle: '56 items',
icon: 'π',
data: clothingProducts,
},
{
title: 'Books',
subtitle: '12 items',
icon: 'π',
collapsible: true,
data: bookProducts,
},
];
β οΈ The data Property is Required
Every section object must have a data property that's an array. Even for empty sections:
// β Wrong - missing data
const badSection = { title: 'Empty' };
// β
Correct - empty array
const goodSection = { title: 'Empty', data: [] };
Basic Implementation
SectionList has a similar API to FlatList, with additional props for handling sections.
Minimal Example
import { SectionList, View, Text, StyleSheet } from 'react-native';
const DATA = [
{
title: 'Fruits',
data: ['Apple', 'Banana', 'Orange'],
},
{
title: 'Vegetables',
data: ['Carrot', 'Broccoli', 'Spinach'],
},
];
function BasicSectionList() {
return (
<SectionList
sections={DATA}
keyExtractor={(item, index) => item + index}
renderItem={({ item }) => (
<View style={styles.item}>
<Text>{item}</Text>
</View>
)}
renderSectionHeader={({ section: { title } }) => (
<View style={styles.header}>
<Text style={styles.headerText}>{title}</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
header: {
backgroundColor: '#f4f4f4',
padding: 12,
},
headerText: {
fontSize: 16,
fontWeight: 'bold',
},
item: {
padding: 16,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
});
Required Props
π SectionList Required Props
| Prop | Type | Description |
|---|---|---|
sections |
Array<Section> |
Array of section objects with data arrays |
renderItem |
Function |
Renders each item within sections |
The renderItem Function
SectionList's renderItem receives more information than FlatList's:
// renderItem receives section info too
<SectionList
sections={sections}
renderItem={({ item, index, section, separators }) => {
// item: The current data item
// index: Position within the section (not global)
// section: The entire section object
// separators: Same as FlatList
return (
<View>
<Text>{item.name}</Text>
<Text style={styles.sectionLabel}>
In section: {section.title}
</Text>
{index === 0 && (
<Text style={styles.firstBadge}>First in section</Text>
)}
</View>
);
}}
/>
Section Headers
Section headers are what make SectionList special. They appear before each group of items.
renderSectionHeader
// Basic section header
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={({ section }) => (
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{section.title}</Text>
</View>
)}
/>
// Header with metadata
interface ProductSection {
title: string;
icon: string;
itemCount: number;
data: Product[];
}
<SectionList
sections={productSections}
renderItem={renderProduct}
renderSectionHeader={({ section }) => (
<View style={styles.sectionHeader}>
<Text style={styles.sectionIcon}>{section.icon}</Text>
<View style={styles.sectionInfo}>
<Text style={styles.sectionTitle}>{section.title}</Text>
<Text style={styles.sectionCount}>
{section.data.length} items
</Text>
</View>
</View>
)}
/>
Conditional Headers
// Only show header if section has items
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={({ section }) => {
// Don't render header for empty sections
if (section.data.length === 0) {
return null;
}
return (
<View style={styles.sectionHeader}>
<Text>{section.title}</Text>
</View>
);
}}
/>
// Different header styles per section
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={({ section }) => {
const isHighlighted = section.highlighted;
return (
<View style={[
styles.sectionHeader,
isHighlighted && styles.highlightedHeader,
]}>
<Text style={[
styles.sectionTitle,
isHighlighted && styles.highlightedTitle,
]}>
{section.title}
</Text>
</View>
);
}}
/>
Interactive Headers
// Collapsible sections
function CollapsibleSectionList() {
const [collapsedSections, setCollapsedSections] = useState<Set<string>>(
new Set()
);
const toggleSection = (title: string) => {
setCollapsedSections(prev => {
const next = new Set(prev);
if (next.has(title)) {
next.delete(title);
} else {
next.add(title);
}
return next;
});
};
// Filter out collapsed section data
const visibleSections = sections.map(section => ({
...section,
data: collapsedSections.has(section.title) ? [] : section.data,
}));
return (
<SectionList
sections={visibleSections}
renderItem={renderItem}
renderSectionHeader={({ section }) => (
<Pressable
style={styles.sectionHeader}
onPress={() => toggleSection(section.title)}
>
<Text style={styles.sectionTitle}>{section.title}</Text>
<Text style={styles.collapseIcon}>
{collapsedSections.has(section.title) ? 'βΆ' : 'βΌ'}
</Text>
</Pressable>
)}
/>
);
}
Sticky Section Headers
Sticky headers remain visible at the top while scrolling through their section. This is one of SectionList's most powerful features.
Enabling Sticky Headers
// Sticky headers are ON by default on iOS, OFF on Android
// Explicitly enable for consistency:
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
stickySectionHeadersEnabled={true} // Enable sticky headers
/>
// Disable sticky headers:
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
stickySectionHeadersEnabled={false}
/>
Styling Sticky Headers
// Add shadow when header is stuck
const styles = StyleSheet.create({
sectionHeader: {
backgroundColor: '#fff',
paddingVertical: 10,
paddingHorizontal: 16,
borderBottomWidth: 1,
borderBottomColor: '#e0e0e0',
// iOS shadow
shadowColor: '#000',
shadowOffset: { width: 0, height: 2 },
shadowOpacity: 0.1,
shadowRadius: 3,
// Android elevation
elevation: 3,
},
sectionTitle: {
fontSize: 14,
fontWeight: '600',
color: '#333',
textTransform: 'uppercase',
letterSpacing: 0.5,
},
});
// Compact contact-list style headers
const contactHeaderStyles = StyleSheet.create({
header: {
backgroundColor: '#f7f7f7',
paddingVertical: 6,
paddingHorizontal: 16,
},
letter: {
fontSize: 14,
fontWeight: 'bold',
color: '#6200ee',
},
});
β Sticky Header Best Practices
- Keep headers short: Tall sticky headers take up too much screen space
- Use subtle backgrounds: Solid backgrounds prevent content showing through
- Add shadows: Helps users see the header is elevated above content
- Enable on both platforms: Set
stickySectionHeadersEnabledexplicitly for consistency
Separators
SectionList provides granular control over separators with different components for items within sections and between sections.
ItemSeparatorComponent
Renders between items within a section (not after the last item):
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
ItemSeparatorComponent={() => (
<View style={styles.itemSeparator} />
)}
/>
const styles = StyleSheet.create({
itemSeparator: {
height: 1,
backgroundColor: '#e0e0e0',
marginLeft: 16, // Inset for iOS style
},
});
SectionSeparatorComponent
Renders between sections (after section header and before next section's header):
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
SectionSeparatorComponent={() => (
<View style={styles.sectionSeparator} />
)}
/>
const styles = StyleSheet.create({
sectionSeparator: {
height: 24,
backgroundColor: '#f5f5f5',
},
});
Understanding Separator Placement
Context-Aware Separators
Separator components receive information about their position:
// ItemSeparatorComponent with context
<SectionList
sections={sections}
renderItem={renderItem}
ItemSeparatorComponent={({ highlighted, leadingItem, trailingItem, section }) => {
// highlighted: true if adjacent item is highlighted
// leadingItem: the item above this separator
// trailingItem: the item below this separator
// section: the section containing these items
return (
<View style={[
styles.separator,
highlighted && styles.separatorHighlighted,
]} />
);
}}
/>
// SectionSeparatorComponent with context
<SectionList
sections={sections}
renderItem={renderItem}
SectionSeparatorComponent={({ leadingItem, leadingSection, trailingItem, trailingSection }) => {
// Render different separator after different sections
if (leadingSection?.title === 'Featured') {
return <View style={styles.featuredSeparator} />;
}
return <View style={styles.normalSeparator} />;
}}
/>
Common Separator Patterns
// Pattern 1: Inset separators (iOS style)
const InsetSeparator = () => (
<View style={{
height: StyleSheet.hairlineWidth,
backgroundColor: '#c6c6c8',
marginLeft: 60, // Inset from left
}} />
);
// Pattern 2: Full-width separators
const FullSeparator = () => (
<View style={{
height: 1,
backgroundColor: '#e0e0e0',
}} />
);
// Pattern 3: Spacing separators
const SpacingSeparator = () => (
<View style={{ height: 12 }} />
);
// Pattern 4: Section gap with background
const SectionGap = () => (
<View style={{
height: 20,
backgroundColor: '#f5f5f5',
}} />
);
TypeScript Patterns
TypeScript helps catch data structure mistakes early and provides excellent autocomplete when working with sections.
Typing Sections
import { SectionList, SectionListData, SectionListRenderItem } from 'react-native';
// Define your item type
interface Contact {
id: string;
name: string;
phone: string;
avatar: string;
}
// Define your section type (extends the base with custom properties)
interface ContactSection {
title: string;
data: Contact[];
}
// Type the SectionList
function TypedContactList() {
const sections: ContactSection[] = [
{
title: 'A',
data: [
{ id: '1', name: 'Alice', phone: '555-0001', avatar: '...' },
],
},
];
const renderItem: SectionListRenderItem<Contact, ContactSection> = ({ item, section }) => (
<View>
<Text>{item.name}</Text> {/* item is Contact */}
<Text>Section: {section.title}</Text> {/* section is ContactSection */}
</View>
);
return (
<SectionList<Contact, ContactSection>
sections={sections}
renderItem={renderItem}
renderSectionHeader={({ section }) => (
<Text>{section.title}</Text> {/* section.title is typed */}
)}
keyExtractor={(item) => item.id} {/* item.id is typed */}
/>
);
}
Using SectionListData
import { SectionListData } from 'react-native';
// SectionListData is the built-in type for sections
// It requires a data property and allows custom properties
interface Product {
id: string;
name: string;
price: number;
}
// Using SectionListData with custom section properties
type ProductSection = SectionListData<Product> & {
title: string;
icon: string;
};
const sections: ProductSection[] = [
{
title: 'Electronics',
icon: 'π±',
data: [
{ id: '1', name: 'Phone', price: 999 },
{ id: '2', name: 'Laptop', price: 1499 },
],
},
];
// Alternative: Define your own section type
interface MySection<T> {
title: string;
subtitle?: string;
data: T[];
}
const mySections: MySection<Product>[] = [...];
Typing Render Functions
import {
SectionListRenderItem,
SectionListRenderItemInfo,
} from 'react-native';
// Option 1: Use the SectionListRenderItem type
const renderItem: SectionListRenderItem<Contact, ContactSection> = (info) => {
const { item, index, section, separators } = info;
return <ContactCard contact={item} />;
};
// Option 2: Type the parameter directly
const renderItem = ({
item,
section
}: SectionListRenderItemInfo<Contact, ContactSection>) => {
return <ContactCard contact={item} sectionTitle={section.title} />;
};
// Option 3: Inline with generic
<SectionList<Contact, ContactSection>
sections={sections}
renderItem={({ item }) => <ContactCard contact={item} />}
/>
Generic Section List Component
// Create a reusable typed section list
interface GenericSection<T> {
title: string;
data: T[];
}
interface GroupedListProps<T> {
sections: GenericSection<T>[];
renderItem: (item: T) => React.ReactElement;
keyExtractor: (item: T) => string;
}
function GroupedList<T>({
sections,
renderItem,
keyExtractor
}: GroupedListProps<T>) {
return (
<SectionList<T, GenericSection<T>>
sections={sections}
keyExtractor={keyExtractor}
renderItem={({ item }) => renderItem(item)}
renderSectionHeader={({ section }) => (
<View style={styles.header}>
<Text style={styles.headerText}>{section.title}</Text>
</View>
)}
stickySectionHeadersEnabled
/>
);
}
// Usage
<GroupedList<Contact>
sections={contactSections}
renderItem={(contact) => <ContactRow contact={contact} />}
keyExtractor={(contact) => contact.id}
/>
β TypeScript Tips
- Always type your items: Prevents rendering bugs and enables autocomplete
- Extend section types: Add custom properties like
title,icon, etc. - Use the generic:
<SectionList<ItemType, SectionType> - Type render functions: Use
SectionListRenderItemfor explicit typing
Common Patterns
Let's implement the most common SectionList use cases you'll encounter in real apps.
Pattern 1: Contacts List (A-Z)
import React, { useMemo, useCallback } from 'react';
import { SectionList, View, Text, Image, StyleSheet } from 'react-native';
interface Contact {
id: string;
name: string;
phone: string;
avatar: string;
}
interface ContactSection {
title: string;
data: Contact[];
}
// Group contacts alphabetically
const groupContacts = (contacts: Contact[]): ContactSection[] => {
const sorted = [...contacts].sort((a, b) =>
a.name.localeCompare(b.name)
);
const groups: Record<string, Contact[]> = {};
sorted.forEach(contact => {
const letter = contact.name[0].toUpperCase();
if (!groups[letter]) groups[letter] = [];
groups[letter].push(contact);
});
return Object.entries(groups)
.sort(([a], [b]) => a.localeCompare(b))
.map(([letter, contacts]) => ({
title: letter,
data: contacts,
}));
};
const ContactItem = React.memo(({ contact }: { contact: Contact }) => (
<View style={styles.contactItem}>
<Image source={{ uri: contact.avatar }} style={styles.avatar} />
<View style={styles.contactInfo}>
<Text style={styles.name}>{contact.name}</Text>
<Text style={styles.phone}>{contact.phone}</Text>
</View>
</View>
));
export default function ContactsList({ contacts }: { contacts: Contact[] }) {
const sections = useMemo(() => groupContacts(contacts), [contacts]);
const renderItem = useCallback(
({ item }: { item: Contact }) => <ContactItem contact={item} />,
[]
);
const renderSectionHeader = useCallback(
({ section }: { section: ContactSection }) => (
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{section.title}</Text>
</View>
),
[]
);
return (
<SectionList<Contact, ContactSection>
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={(item) => item.id}
stickySectionHeadersEnabled
ItemSeparatorComponent={() => <View style={styles.separator} />}
/>
);
}
const styles = StyleSheet.create({
sectionHeader: {
backgroundColor: '#f7f7f7',
paddingVertical: 6,
paddingHorizontal: 16,
},
sectionTitle: {
fontSize: 14,
fontWeight: 'bold',
color: '#6200ee',
},
contactItem: {
flexDirection: 'row',
alignItems: 'center',
padding: 12,
paddingHorizontal: 16,
backgroundColor: '#fff',
},
avatar: {
width: 44,
height: 44,
borderRadius: 22,
marginRight: 12,
},
contactInfo: {
flex: 1,
},
name: {
fontSize: 16,
fontWeight: '500',
},
phone: {
fontSize: 14,
color: '#666',
marginTop: 2,
},
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: '#c6c6c8',
marginLeft: 72,
},
});
Pattern 2: Settings Screen
import React, { useCallback } from 'react';
import {
SectionList,
View,
Text,
Switch,
Pressable,
StyleSheet
} from 'react-native';
type SettingType = 'toggle' | 'navigate' | 'action';
interface Setting {
id: string;
label: string;
type: SettingType;
value?: boolean;
icon?: string;
danger?: boolean;
}
interface SettingsSection {
title: string;
data: Setting[];
}
const SETTINGS: SettingsSection[] = [
{
title: 'Account',
data: [
{ id: 'profile', label: 'Edit Profile', type: 'navigate', icon: 'π€' },
{ id: 'password', label: 'Change Password', type: 'navigate', icon: 'π' },
{ id: 'email', label: 'Email Preferences', type: 'navigate', icon: 'π§' },
],
},
{
title: 'Preferences',
data: [
{ id: 'notifications', label: 'Push Notifications', type: 'toggle', value: true, icon: 'π' },
{ id: 'darkMode', label: 'Dark Mode', type: 'toggle', value: false, icon: 'π' },
{ id: 'sounds', label: 'Sound Effects', type: 'toggle', value: true, icon: 'π' },
],
},
{
title: 'Support',
data: [
{ id: 'help', label: 'Help Center', type: 'navigate', icon: 'β' },
{ id: 'feedback', label: 'Send Feedback', type: 'navigate', icon: 'π¬' },
{ id: 'about', label: 'About', type: 'navigate', icon: 'βΉοΈ' },
],
},
{
title: '',
data: [
{ id: 'logout', label: 'Log Out', type: 'action', icon: 'πͺ', danger: true },
],
},
];
const SettingRow = ({
setting,
onToggle,
onPress
}: {
setting: Setting;
onToggle: (id: string, value: boolean) => void;
onPress: (id: string) => void;
}) => {
const content = (
<View style={styles.settingRow}>
<Text style={styles.settingIcon}>{setting.icon}</Text>
<Text style={[
styles.settingLabel,
setting.danger && styles.dangerLabel,
]}>
{setting.label}
</Text>
{setting.type === 'toggle' && (
<Switch
value={setting.value}
onValueChange={(value) => onToggle(setting.id, value)}
/>
)}
{setting.type === 'navigate' && (
<Text style={styles.chevron}>βΊ</Text>
)}
</View>
);
if (setting.type === 'toggle') {
return content;
}
return (
<Pressable onPress={() => onPress(setting.id)}>
{content}
</Pressable>
);
};
export default function SettingsScreen() {
const handleToggle = useCallback((id: string, value: boolean) => {
console.log(`Toggle ${id}: ${value}`);
}, []);
const handlePress = useCallback((id: string) => {
console.log(`Navigate/Action: ${id}`);
}, []);
const renderItem = useCallback(
({ item }: { item: Setting }) => (
<SettingRow
setting={item}
onToggle={handleToggle}
onPress={handlePress}
/>
),
[handleToggle, handlePress]
);
const renderSectionHeader = useCallback(
({ section }: { section: SettingsSection }) => {
if (!section.title) return null;
return (
<View style={styles.sectionHeader}>
<Text style={styles.sectionTitle}>{section.title}</Text>
</View>
);
},
[]
);
return (
<SectionList<Setting, SettingsSection>
sections={SETTINGS}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={(item) => item.id}
ItemSeparatorComponent={() => <View style={styles.separator} />}
SectionSeparatorComponent={() => <View style={styles.sectionGap} />}
contentContainerStyle={styles.container}
/>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#f2f2f7',
},
sectionHeader: {
paddingHorizontal: 16,
paddingTop: 24,
paddingBottom: 8,
},
sectionTitle: {
fontSize: 13,
fontWeight: '600',
color: '#666',
textTransform: 'uppercase',
},
settingRow: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
paddingVertical: 12,
paddingHorizontal: 16,
},
settingIcon: {
fontSize: 20,
marginRight: 12,
},
settingLabel: {
flex: 1,
fontSize: 16,
},
dangerLabel: {
color: '#f44336',
},
chevron: {
fontSize: 20,
color: '#c7c7cc',
},
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: '#c6c6c8',
marginLeft: 52,
},
sectionGap: {
height: 8,
},
});
Pattern 3: Events by Date
import React, { useMemo, useCallback } from 'react';
import { SectionList, View, Text, StyleSheet } from 'react-native';
interface CalendarEvent {
id: string;
title: string;
time: string;
location?: string;
color: string;
}
interface EventSection {
title: string;
date: Date;
data: CalendarEvent[];
}
// Group events by date
const groupEventsByDate = (events: CalendarEvent[]): EventSection[] => {
// In a real app, events would have date objects
// This is a simplified example
return [
{
title: 'Today',
date: new Date(),
data: events.slice(0, 3),
},
{
title: 'Tomorrow',
date: new Date(Date.now() + 86400000),
data: events.slice(3, 5),
},
{
title: 'This Week',
date: new Date(Date.now() + 172800000),
data: events.slice(5),
},
];
};
const EventCard = React.memo(({ event }: { event: CalendarEvent }) => (
<View style={styles.eventCard}>
<View style={[styles.colorBar, { backgroundColor: event.color }]} />
<View style={styles.eventContent}>
<Text style={styles.eventTime}>{event.time}</Text>
<Text style={styles.eventTitle}>{event.title}</Text>
{event.location && (
<Text style={styles.eventLocation}>π {event.location}</Text>
)}
</View>
</View>
));
export default function EventsCalendar() {
const events: CalendarEvent[] = [
{ id: '1', title: 'Team Standup', time: '9:00 AM', location: 'Zoom', color: '#4CAF50' },
{ id: '2', title: 'Product Review', time: '11:00 AM', location: 'Room 3B', color: '#2196F3' },
{ id: '3', title: 'Lunch with Alex', time: '12:30 PM', location: 'CafΓ© Luna', color: '#FF9800' },
{ id: '4', title: 'Client Call', time: '2:00 PM', color: '#f44336' },
{ id: '5', title: 'Sprint Planning', time: '4:00 PM', location: 'Main Office', color: '#9C27B0' },
{ id: '6', title: 'Dentist Appointment', time: '10:00 AM', location: 'Dr. Smith', color: '#607D8B' },
{ id: '7', title: 'Birthday Party', time: '6:00 PM', location: 'Tom\'s Place', color: '#E91E63' },
];
const sections = useMemo(() => groupEventsByDate(events), [events]);
const renderItem = useCallback(
({ item }: { item: CalendarEvent }) => <EventCard event={item} />,
[]
);
const renderSectionHeader = useCallback(
({ section }: { section: EventSection }) => (
<View style={styles.dateHeader}>
<Text style={styles.dateTitle}>{section.title}</Text>
<Text style={styles.dateSubtitle}>
{section.date.toLocaleDateString('en-US', {
weekday: 'long',
month: 'short',
day: 'numeric'
})}
</Text>
</View>
),
[]
);
const renderSectionFooter = useCallback(
({ section }: { section: EventSection }) => (
<View style={styles.sectionFooter}>
<Text style={styles.eventCount}>
{section.data.length} event{section.data.length !== 1 ? 's' : ''}
</Text>
</View>
),
[]
);
return (
<SectionList<CalendarEvent, EventSection>
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
renderSectionFooter={renderSectionFooter}
keyExtractor={(item) => item.id}
stickySectionHeadersEnabled
ItemSeparatorComponent={() => <View style={{ height: 8 }} />}
SectionSeparatorComponent={() => <View style={{ height: 16 }} />}
contentContainerStyle={styles.container}
/>
);
}
const styles = StyleSheet.create({
container: {
padding: 16,
backgroundColor: '#f5f5f5',
},
dateHeader: {
backgroundColor: '#f5f5f5',
paddingVertical: 8,
},
dateTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#333',
},
dateSubtitle: {
fontSize: 14,
color: '#666',
marginTop: 2,
},
eventCard: {
flexDirection: 'row',
backgroundColor: '#fff',
borderRadius: 12,
overflow: 'hidden',
elevation: 2,
shadowColor: '#000',
shadowOffset: { width: 0, height: 1 },
shadowOpacity: 0.1,
shadowRadius: 2,
},
colorBar: {
width: 4,
},
eventContent: {
flex: 1,
padding: 12,
},
eventTime: {
fontSize: 13,
fontWeight: '600',
color: '#6200ee',
},
eventTitle: {
fontSize: 16,
fontWeight: '500',
marginTop: 4,
},
eventLocation: {
fontSize: 13,
color: '#666',
marginTop: 4,
},
sectionFooter: {
paddingTop: 8,
alignItems: 'flex-end',
},
eventCount: {
fontSize: 12,
color: '#999',
},
});
Performance Considerations
SectionList inherits FlatList's virtualization, but there are some additional considerations for sectioned data.
getItemLayout with Sections
Unlike FlatList, implementing getItemLayout for SectionList is complex because you need to account for section headers:
// Simple case: all items and headers have fixed heights
const HEADER_HEIGHT = 40;
const ITEM_HEIGHT = 60;
// This gets complicated with sections!
// You need to calculate the cumulative offset including all previous
// sections' headers and items
const getItemLayout = (
data: SectionListData<Item>[] | null,
index: number
) => {
// For SectionList, the index is a "virtual" index across all items
// This is complex to implement correctly
// A helper function to calculate:
let offset = 0;
let itemIndex = 0;
if (!data) {
return { length: ITEM_HEIGHT, offset: 0, index };
}
for (const section of data) {
// Add section header height
offset += HEADER_HEIGHT;
for (const item of section.data) {
if (itemIndex === index) {
return { length: ITEM_HEIGHT, offset, index };
}
offset += ITEM_HEIGHT;
itemIndex++;
}
}
return { length: ITEM_HEIGHT, offset, index };
};
// β οΈ This is simplified - real implementation needs to handle
// separators, footers, and empty sections
β οΈ getItemLayout Complexity
Due to the complexity of calculating offsets with variable section sizes, many developers skip getItemLayout for SectionList unless performance is critical. Focus on other optimizations first:
- Memoize section data transformations
- Use
React.memoon item components - Memoize render functions with
useCallback - Keep section headers simple and lightweight
Memoizing Section Data
// β Bad: Creates new sections array every render
function BadExample({ contacts }) {
// This runs every render!
const sections = groupContactsByLetter(contacts);
return <SectionList sections={sections} ... />;
}
// β
Good: Memoize the transformation
function GoodExample({ contacts }) {
const sections = useMemo(
() => groupContactsByLetter(contacts),
[contacts] // Only recalculate when contacts change
);
return <SectionList sections={sections} ... />;
}
// β
Even better: Memoize deeply
function BetterExample({ contacts }) {
// Memoize the grouping
const sections = useMemo(
() => groupContactsByLetter(contacts),
[contacts]
);
// Memoize render functions
const renderItem = useCallback(
({ item }) => <ContactRow contact={item} />,
[]
);
const renderSectionHeader = useCallback(
({ section }) => <SectionHeader title={section.title} />,
[]
);
// Memoize item component
const ContactRow = memo(({ contact }) => (
<View><Text>{contact.name}</Text></View>
));
return (
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={keyExtractor}
/>
);
}
Performance Props
SectionList supports the same performance props as FlatList:
<SectionList
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
// Virtualization tuning
windowSize={11}
maxToRenderPerBatch={10}
initialNumToRender={10}
updateCellsBatchingPeriod={50}
// Remove off-screen views (Android)
removeClippedSubviews={true}
// Extra data for re-render triggers
extraData={selectedId}
/>
Large Number of Sections
// If you have many sections (e.g., 26 alphabet sections),
// consider whether SectionList is the right choice
// Option 1: Flatten for very large datasets
// Use FlatList with inline headers in your data
const flattenedData = sections.flatMap(section => [
{ type: 'header', title: section.title },
...section.data.map(item => ({ type: 'item', ...item })),
]);
<FlatList
data={flattenedData}
renderItem={({ item }) => {
if (item.type === 'header') {
return <SectionHeader title={item.title} />;
}
return <ItemRow item={item} />;
}}
getItemLayout={...} // Easier to implement
/>
// Option 2: Use FlashList (covered in next lesson)
// FlashList handles sections more efficiently
β SectionList Performance Checklist
- β Memoize section data with
useMemo - β Memoize
renderItemandrenderSectionHeaderwithuseCallback - β Wrap item components with
React.memo - β Keep section headers lightweight (minimal components)
- β Use
keyExtractorwith unique, stable keys - β Consider
windowSizetuning for very long lists - β Avoid inline functions and styles
Hands-On Exercises
Practice building real-world sectioned lists.
Exercise 1: Music Library
Build a music library with songs grouped by album.
Requirements:
- Group songs by album
- Section header shows album name and cover art
- Each song shows title, artist, and duration
- Sticky section headers
- Section footer shows total album duration
π‘ Hint
Create an AlbumSection interface with album metadata. Calculate total duration in the footer by summing song durations in section.data.
β Solution
import React, { useCallback } from 'react';
import { SectionList, View, Text, Image, StyleSheet } from 'react-native';
interface Song {
id: string;
title: string;
artist: string;
duration: number; // seconds
}
interface AlbumSection {
title: string;
artist: string;
coverArt: string;
year: number;
data: Song[];
}
const formatDuration = (seconds: number): string => {
const mins = Math.floor(seconds / 60);
const secs = seconds % 60;
return `${mins}:${secs.toString().padStart(2, '0')}`;
};
const ALBUMS: AlbumSection[] = [
{
title: 'Abbey Road',
artist: 'The Beatles',
coverArt: 'https://picsum.photos/seed/album1/100',
year: 1969,
data: [
{ id: '1', title: 'Come Together', artist: 'The Beatles', duration: 259 },
{ id: '2', title: 'Something', artist: 'The Beatles', duration: 182 },
{ id: '3', title: 'Here Comes The Sun', artist: 'The Beatles', duration: 185 },
],
},
{
title: 'Thriller',
artist: 'Michael Jackson',
coverArt: 'https://picsum.photos/seed/album2/100',
year: 1982,
data: [
{ id: '4', title: 'Wanna Be Startin\' Somethin\'', artist: 'Michael Jackson', duration: 363 },
{ id: '5', title: 'Thriller', artist: 'Michael Jackson', duration: 357 },
{ id: '6', title: 'Beat It', artist: 'Michael Jackson', duration: 258 },
{ id: '7', title: 'Billie Jean', artist: 'Michael Jackson', duration: 294 },
],
},
{
title: 'Back in Black',
artist: 'AC/DC',
coverArt: 'https://picsum.photos/seed/album3/100',
year: 1980,
data: [
{ id: '8', title: 'Hells Bells', artist: 'AC/DC', duration: 312 },
{ id: '9', title: 'Back in Black', artist: 'AC/DC', duration: 255 },
{ id: '10', title: 'You Shook Me All Night Long', artist: 'AC/DC', duration: 210 },
],
},
];
const SongRow = React.memo(({ song, index }: { song: Song; index: number }) => (
<View style={styles.songRow}>
<Text style={styles.songNumber}>{index + 1}</Text>
<View style={styles.songInfo}>
<Text style={styles.songTitle}>{song.title}</Text>
<Text style={styles.songArtist}>{song.artist}</Text>
</View>
<Text style={styles.songDuration}>{formatDuration(song.duration)}</Text>
</View>
));
export default function MusicLibrary() {
const renderItem = useCallback(
({ item, index }: { item: Song; index: number }) => (
<SongRow song={item} index={index} />
),
[]
);
const renderSectionHeader = useCallback(
({ section }: { section: AlbumSection }) => (
<View style={styles.albumHeader}>
<Image source={{ uri: section.coverArt }} style={styles.albumCover} />
<View style={styles.albumInfo}>
<Text style={styles.albumTitle}>{section.title}</Text>
<Text style={styles.albumArtist}>{section.artist}</Text>
<Text style={styles.albumYear}>{section.year}</Text>
</View>
</View>
),
[]
);
const renderSectionFooter = useCallback(
({ section }: { section: AlbumSection }) => {
const totalSeconds = section.data.reduce((sum, song) => sum + song.duration, 0);
const mins = Math.floor(totalSeconds / 60);
return (
<View style={styles.albumFooter}>
<Text style={styles.footerText}>
{section.data.length} songs Β· {mins} minutes
</Text>
</View>
);
},
[]
);
return (
<SectionList<Song, AlbumSection>
sections={ALBUMS}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
renderSectionFooter={renderSectionFooter}
keyExtractor={(item) => item.id}
stickySectionHeadersEnabled
ItemSeparatorComponent={() => <View style={styles.separator} />}
SectionSeparatorComponent={() => <View style={styles.sectionGap} />}
contentContainerStyle={styles.container}
/>
);
}
const styles = StyleSheet.create({
container: {
backgroundColor: '#1a1a1a',
},
albumHeader: {
flexDirection: 'row',
padding: 16,
backgroundColor: '#1a1a1a',
alignItems: 'center',
},
albumCover: {
width: 80,
height: 80,
borderRadius: 4,
},
albumInfo: {
marginLeft: 16,
flex: 1,
},
albumTitle: {
fontSize: 20,
fontWeight: 'bold',
color: '#fff',
},
albumArtist: {
fontSize: 16,
color: '#ccc',
marginTop: 4,
},
albumYear: {
fontSize: 14,
color: '#888',
marginTop: 2,
},
songRow: {
flexDirection: 'row',
alignItems: 'center',
paddingVertical: 12,
paddingHorizontal: 16,
backgroundColor: '#1a1a1a',
},
songNumber: {
width: 30,
fontSize: 14,
color: '#888',
},
songInfo: {
flex: 1,
},
songTitle: {
fontSize: 16,
color: '#fff',
},
songArtist: {
fontSize: 13,
color: '#888',
marginTop: 2,
},
songDuration: {
fontSize: 14,
color: '#888',
},
separator: {
height: StyleSheet.hairlineWidth,
backgroundColor: '#333',
marginLeft: 46,
},
sectionGap: {
height: 24,
},
albumFooter: {
paddingHorizontal: 16,
paddingVertical: 8,
backgroundColor: '#1a1a1a',
},
footerText: {
fontSize: 13,
color: '#666',
},
});
Exercise 2: FAQ Accordion
Build an FAQ page with collapsible section categories.
Requirements:
- FAQ items grouped by category (Account, Billing, Technical)
- Tap section header to collapse/expand that category
- Tap FAQ item to expand/collapse the answer
- Smooth visual feedback for expanded/collapsed states
- Track expanded states for both sections and items
π‘ Hint
Use two pieces of state: collapsedSections (Set of collapsed section titles) and expandedItems (Set of expanded FAQ IDs). Filter section data based on collapsed state.
β Solution
import React, { useState, useCallback, useMemo } from 'react';
import {
SectionList,
View,
Text,
Pressable,
StyleSheet,
LayoutAnimation,
Platform,
UIManager,
} from 'react-native';
// Enable LayoutAnimation on Android
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
interface FAQItem {
id: string;
question: string;
answer: string;
}
interface FAQSection {
title: string;
icon: string;
data: FAQItem[];
}
const FAQ_DATA: FAQSection[] = [
{
title: 'Account',
icon: 'π€',
data: [
{ id: '1', question: 'How do I reset my password?', answer: 'Go to Settings > Account > Change Password. Enter your current password, then create a new one.' },
{ id: '2', question: 'Can I change my username?', answer: 'Yes! Navigate to Settings > Profile > Edit Username. Note: You can only change it once every 30 days.' },
{ id: '3', question: 'How do I delete my account?', answer: 'Go to Settings > Account > Delete Account. Please note this action is permanent and cannot be undone.' },
],
},
{
title: 'Billing',
icon: 'π³',
data: [
{ id: '4', question: 'What payment methods do you accept?', answer: 'We accept Visa, MasterCard, American Express, PayPal, and Apple Pay.' },
{ id: '5', question: 'How do I get a refund?', answer: 'Contact our support team within 14 days of purchase. Refunds are processed within 5-7 business days.' },
],
},
{
title: 'Technical',
icon: 'π§',
data: [
{ id: '6', question: 'The app is crashing. What should I do?', answer: 'Try these steps: 1) Force close and reopen the app, 2) Clear app cache, 3) Update to the latest version, 4) Reinstall the app.' },
{ id: '7', question: 'How do I enable notifications?', answer: 'Go to your device Settings > Notifications > [App Name] and toggle on Allow Notifications.' },
{ id: '8', question: 'Is my data synced across devices?', answer: 'Yes! As long as you\'re signed in with the same account, your data syncs automatically.' },
],
},
];
const FAQItemRow = React.memo(({
item,
isExpanded,
onToggle
}: {
item: FAQItem;
isExpanded: boolean;
onToggle: () => void;
}) => (
<Pressable style={styles.faqItem} onPress={onToggle}>
<View style={styles.questionRow}>
<Text style={styles.question}>{item.question}</Text>
<Text style={styles.expandIcon}>{isExpanded ? 'β' : '+'}</Text>
</View>
{isExpanded && (
<Text style={styles.answer}>{item.answer}</Text>
)}
</Pressable>
));
export default function FAQScreen() {
const [collapsedSections, setCollapsedSections] = useState<Set<string>>(new Set());
const [expandedItems, setExpandedItems] = useState<Set<string>>(new Set());
const toggleSection = useCallback((title: string) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setCollapsedSections(prev => {
const next = new Set(prev);
if (next.has(title)) {
next.delete(title);
} else {
next.add(title);
}
return next;
});
}, []);
const toggleItem = useCallback((id: string) => {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
setExpandedItems(prev => {
const next = new Set(prev);
if (next.has(id)) {
next.delete(id);
} else {
next.add(id);
}
return next;
});
}, []);
// Filter out collapsed sections' data
const sections = useMemo(() =>
FAQ_DATA.map(section => ({
...section,
data: collapsedSections.has(section.title) ? [] : section.data,
})),
[collapsedSections]
);
const renderItem = useCallback(
({ item }: { item: FAQItem }) => (
<FAQItemRow
item={item}
isExpanded={expandedItems.has(item.id)}
onToggle={() => toggleItem(item.id)}
/>
),
[expandedItems, toggleItem]
);
const renderSectionHeader = useCallback(
({ section }: { section: FAQSection }) => {
const isCollapsed = collapsedSections.has(section.title);
const originalSection = FAQ_DATA.find(s => s.title === section.title);
const itemCount = originalSection?.data.length ?? 0;
return (
<Pressable
style={styles.sectionHeader}
onPress={() => toggleSection(section.title)}
>
<Text style={styles.sectionIcon}>{section.icon}</Text>
<View style={styles.sectionInfo}>
<Text style={styles.sectionTitle}>{section.title}</Text>
<Text style={styles.sectionCount}>{itemCount} questions</Text>
</View>
<Text style={styles.collapseIcon}>{isCollapsed ? 'βΆ' : 'βΌ'}</Text>
</Pressable>
);
},
[collapsedSections, toggleSection]
);
return (
<View style={styles.container}>
<Text style={styles.title}>Frequently Asked Questions</Text>
<SectionList<FAQItem, FAQSection>
sections={sections}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
keyExtractor={(item) => item.id}
stickySectionHeadersEnabled={false}
ItemSeparatorComponent={() => <View style={styles.separator} />}
SectionSeparatorComponent={() => <View style={styles.sectionGap} />}
contentContainerStyle={styles.listContent}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
padding: 16,
backgroundColor: '#fff',
},
listContent: {
padding: 16,
},
sectionHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
padding: 16,
borderRadius: 12,
marginBottom: 8,
},
sectionIcon: {
fontSize: 24,
marginRight: 12,
},
sectionInfo: {
flex: 1,
},
sectionTitle: {
fontSize: 18,
fontWeight: '600',
},
sectionCount: {
fontSize: 13,
color: '#666',
marginTop: 2,
},
collapseIcon: {
fontSize: 14,
color: '#999',
},
faqItem: {
backgroundColor: '#fff',
padding: 16,
borderRadius: 8,
},
questionRow: {
flexDirection: 'row',
alignItems: 'flex-start',
},
question: {
flex: 1,
fontSize: 16,
fontWeight: '500',
lineHeight: 22,
},
expandIcon: {
fontSize: 20,
color: '#6200ee',
marginLeft: 12,
fontWeight: 'bold',
},
answer: {
fontSize: 14,
color: '#666',
lineHeight: 22,
marginTop: 12,
paddingTop: 12,
borderTopWidth: 1,
borderTopColor: '#eee',
},
separator: {
height: 8,
},
sectionGap: {
height: 16,
},
});
Exercise 3: Shopping Cart
Build a shopping cart grouped by store/seller.
Requirements:
- Group cart items by store
- Section header shows store name and logo
- Each item shows product image, name, price, quantity controls
- Section footer shows store subtotal
- List footer shows grand total
- Empty state when cart is empty
π‘ Hint
Create a StoreSection with store metadata. Use ListFooterComponent for the grand total. Calculate subtotals in section footers.
β Solution
import React, { useState, useCallback, useMemo } from 'react';
import {
SectionList,
View,
Text,
Image,
Pressable,
StyleSheet,
} from 'react-native';
interface CartItem {
id: string;
name: string;
price: number;
quantity: number;
image: string;
}
interface StoreSection {
storeId: string;
storeName: string;
storeLogo: string;
data: CartItem[];
}
const INITIAL_CART: StoreSection[] = [
{
storeId: '1',
storeName: 'TechStore',
storeLogo: 'https://picsum.photos/seed/tech/40',
data: [
{ id: '1', name: 'Wireless Earbuds', price: 79.99, quantity: 1, image: 'https://picsum.photos/seed/p1/80' },
{ id: '2', name: 'Phone Case', price: 19.99, quantity: 2, image: 'https://picsum.photos/seed/p2/80' },
],
},
{
storeId: '2',
storeName: 'Fashion Hub',
storeLogo: 'https://picsum.photos/seed/fashion/40',
data: [
{ id: '3', name: 'Cotton T-Shirt', price: 24.99, quantity: 3, image: 'https://picsum.photos/seed/p3/80' },
{ id: '4', name: 'Denim Jeans', price: 59.99, quantity: 1, image: 'https://picsum.photos/seed/p4/80' },
],
},
{
storeId: '3',
storeName: 'Home Essentials',
storeLogo: 'https://picsum.photos/seed/home/40',
data: [
{ id: '5', name: 'Scented Candle Set', price: 34.99, quantity: 1, image: 'https://picsum.photos/seed/p5/80' },
],
},
];
const CartItemRow = React.memo(({
item,
onUpdateQuantity,
}: {
item: CartItem;
onUpdateQuantity: (id: string, delta: number) => void;
}) => (
<View style={styles.cartItem}>
<Image source={{ uri: item.image }} style={styles.productImage} />
<View style={styles.productInfo}>
<Text style={styles.productName}>{item.name}</Text>
<Text style={styles.productPrice}>${item.price.toFixed(2)}</Text>
</View>
<View style={styles.quantityControls}>
<Pressable
style={styles.qtyButton}
onPress={() => onUpdateQuantity(item.id, -1)}
>
<Text style={styles.qtyButtonText}>β</Text>
</Pressable>
<Text style={styles.quantity}>{item.quantity}</Text>
<Pressable
style={styles.qtyButton}
onPress={() => onUpdateQuantity(item.id, 1)}
>
<Text style={styles.qtyButtonText}>+</Text>
</Pressable>
</View>
</View>
));
export default function ShoppingCart() {
const [cart, setCart] = useState<StoreSection[]>(INITIAL_CART);
const updateQuantity = useCallback((itemId: string, delta: number) => {
setCart(prevCart =>
prevCart.map(store => ({
...store,
data: store.data
.map(item =>
item.id === itemId
? { ...item, quantity: Math.max(0, item.quantity + delta) }
: item
)
.filter(item => item.quantity > 0),
})).filter(store => store.data.length > 0)
);
}, []);
const grandTotal = useMemo(() =>
cart.reduce(
(total, store) =>
total + store.data.reduce((sum, item) => sum + item.price * item.quantity, 0),
0
),
[cart]
);
const renderItem = useCallback(
({ item }: { item: CartItem }) => (
<CartItemRow item={item} onUpdateQuantity={updateQuantity} />
),
[updateQuantity]
);
const renderSectionHeader = useCallback(
({ section }: { section: StoreSection }) => (
<View style={styles.storeHeader}>
<Image source={{ uri: section.storeLogo }} style={styles.storeLogo} />
<Text style={styles.storeName}>{section.storeName}</Text>
</View>
),
[]
);
const renderSectionFooter = useCallback(
({ section }: { section: StoreSection }) => {
const subtotal = section.data.reduce(
(sum, item) => sum + item.price * item.quantity,
0
);
return (
<View style={styles.storeFooter}>
<Text style={styles.subtotalLabel}>Store Subtotal:</Text>
<Text style={styles.subtotalValue}>${subtotal.toFixed(2)}</Text>
</View>
);
},
[]
);
const ListFooter = useMemo(() => (
<View style={styles.grandTotalContainer}>
<View style={styles.grandTotalRow}>
<Text style={styles.grandTotalLabel}>Grand Total</Text>
<Text style={styles.grandTotalValue}>${grandTotal.toFixed(2)}</Text>
</View>
<Pressable style={styles.checkoutButton}>
<Text style={styles.checkoutText}>Proceed to Checkout</Text>
</Pressable>
</View>
), [grandTotal]);
const EmptyCart = useMemo(() => (
<View style={styles.emptyContainer}>
<Text style={styles.emptyIcon}>π</Text>
<Text style={styles.emptyTitle}>Your cart is empty</Text>
<Text style={styles.emptySubtitle}>Add items to get started!</Text>
</View>
), []);
return (
<View style={styles.container}>
<Text style={styles.title}>Shopping Cart</Text>
<SectionList<CartItem, StoreSection>
sections={cart}
renderItem={renderItem}
renderSectionHeader={renderSectionHeader}
renderSectionFooter={renderSectionFooter}
keyExtractor={(item) => item.id}
ListFooterComponent={cart.length > 0 ? ListFooter : null}
ListEmptyComponent={EmptyCart}
ItemSeparatorComponent={() => <View style={styles.itemSeparator} />}
SectionSeparatorComponent={() => <View style={styles.sectionSeparator} />}
contentContainerStyle={styles.listContent}
stickySectionHeadersEnabled
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: '#f5f5f5',
},
title: {
fontSize: 24,
fontWeight: 'bold',
padding: 16,
backgroundColor: '#fff',
},
listContent: {
flexGrow: 1,
},
storeHeader: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
padding: 12,
borderBottomWidth: 1,
borderBottomColor: '#eee',
},
storeLogo: {
width: 32,
height: 32,
borderRadius: 16,
marginRight: 10,
},
storeName: {
fontSize: 16,
fontWeight: '600',
},
cartItem: {
flexDirection: 'row',
alignItems: 'center',
backgroundColor: '#fff',
padding: 12,
},
productImage: {
width: 60,
height: 60,
borderRadius: 8,
},
productInfo: {
flex: 1,
marginLeft: 12,
},
productName: {
fontSize: 15,
fontWeight: '500',
},
productPrice: {
fontSize: 15,
color: '#6200ee',
fontWeight: '600',
marginTop: 4,
},
quantityControls: {
flexDirection: 'row',
alignItems: 'center',
},
qtyButton: {
width: 32,
height: 32,
borderRadius: 16,
backgroundColor: '#f0f0f0',
justifyContent: 'center',
alignItems: 'center',
},
qtyButtonText: {
fontSize: 18,
fontWeight: 'bold',
color: '#333',
},
quantity: {
fontSize: 16,
fontWeight: '600',
marginHorizontal: 12,
minWidth: 24,
textAlign: 'center',
},
storeFooter: {
flexDirection: 'row',
justifyContent: 'space-between',
backgroundColor: '#fafafa',
padding: 12,
},
subtotalLabel: {
fontSize: 14,
color: '#666',
},
subtotalValue: {
fontSize: 14,
fontWeight: '600',
},
itemSeparator: {
height: 1,
backgroundColor: '#eee',
marginLeft: 84,
},
sectionSeparator: {
height: 16,
},
grandTotalContainer: {
padding: 16,
backgroundColor: '#fff',
marginTop: 16,
},
grandTotalRow: {
flexDirection: 'row',
justifyContent: 'space-between',
marginBottom: 16,
},
grandTotalLabel: {
fontSize: 18,
fontWeight: '600',
},
grandTotalValue: {
fontSize: 20,
fontWeight: 'bold',
color: '#6200ee',
},
checkoutButton: {
backgroundColor: '#6200ee',
padding: 16,
borderRadius: 8,
alignItems: 'center',
},
checkoutText: {
color: '#fff',
fontSize: 16,
fontWeight: '600',
},
emptyContainer: {
flex: 1,
justifyContent: 'center',
alignItems: 'center',
padding: 40,
},
emptyIcon: {
fontSize: 64,
marginBottom: 16,
},
emptyTitle: {
fontSize: 20,
fontWeight: '600',
marginBottom: 8,
},
emptySubtitle: {
fontSize: 16,
color: '#666',
},
});
Summary
You've mastered SectionListβthe essential component for rendering grouped data with section headers in React Native.
π― Key Takeaways
- Section structure: Each section needs a
dataarray plus any custom properties - renderSectionHeader: Receives the full section object for rendering headers
- Sticky headers: Use
stickySectionHeadersEnabledfor headers that stay visible - Separators:
ItemSeparatorComponentfor items,SectionSeparatorComponentfor sections - TypeScript: Use
<SectionList<ItemType, SectionType>for type safety - Performance: Memoize section data transformations and render functions
- Use cases: Contacts, settings, calendars, shopping carts, FAQs
SectionList vs FlatList Quick Reference
flowchart TD
A["Do you have grouped data?"] -->|"Yes"| B["Need section headers?"]
A -->|"No"| C["Use FlatList"]
B -->|"Yes"| D["Need sticky headers?"]
B -->|"No"| C
D -->|"Yes"| E["Use SectionList
stickySectionHeadersEnabled"]
D -->|"No"| F["Use SectionList
or FlatList with
inline headers"]
style C fill:#bbdefb
style E fill:#c8e6c9
style F fill:#fff3cd
SectionList Props Cheat Sheet
| Prop | Purpose |
|---|---|
sections |
Array of section objects with data arrays |
renderItem |
Renders each item (receives section info) |
renderSectionHeader |
Renders section header |
renderSectionFooter |
Renders section footer |
stickySectionHeadersEnabled |
Makes section headers sticky |
ItemSeparatorComponent |
Separator between items in a section |
SectionSeparatorComponent |
Separator between sections |
π What's Next?
You now know both FlatList and SectionList inside out! The next lesson introduces FlashListβShopify's high-performance list component that's up to 10x faster than FlatList. You'll learn when and how to use FlashList for the ultimate list performance in demanding applications.