Skip to main content

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

FlatList vs SectionList FlatList Item 1 Item 2 Item 3 Item 4 Item 5 Flat list of items SectionList Section A Item A1 Item A2 Section B Item B1 Item B2 Grouped with headers

πŸ’‘ 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}
/>
Sticky Section Headers Behavior Initial Section A Section B β†’ scroll Scrolling Section A (stuck) Item A3 Section B β†’ scroll In Section B Section B (stuck) Item B2 Item B3 Section C Stuck Normal

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 stickySectionHeadersEnabled explicitly 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

Separator Placement Section A Header SectionSeparator Item A1 ItemSeparator Item A2 Item A3 (last in section) SectionSeparator Section B Header Item B1 Item B2 (No separator after last item) Item Separator Section Separator

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 SectionListRenderItem for 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.memo on 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 renderItem and renderSectionHeader with useCallback
  • ☐ Wrap item components with React.memo
  • ☐ Keep section headers lightweight (minimal components)
  • ☐ Use keyExtractor with unique, stable keys
  • ☐ Consider windowSize tuning 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 data array plus any custom properties
  • renderSectionHeader: Receives the full section object for rendering headers
  • Sticky headers: Use stickySectionHeadersEnabled for headers that stay visible
  • Separators: ItemSeparatorComponent for items, SectionSeparatorComponent for 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.