Skip to main content

SectionFlow Integration Guide

A comprehensive guide for integrating @dreamstack-us/section-flow into DreamStack applications.

Table of Contents


Overview

SectionFlow is a high-performance, section-first list library for React Native that provides:

  • 10x better performance through cell recycling (FlashList-style)
  • Sticky section headers that actually work
  • Reliable scroll methods (scrollToSection, scrollToItem)
  • Full TypeScript support with proper generics
  • Drop-in replacement for React Native's SectionList

Requirements

  • React Native 0.76+ (New Architecture required)
  • React 18.0+
  • Expo SDK 52+ (if using Expo)

Installation

In dreamstack-hq Monorepo

Add to your app's package.json:

{
"dependencies": {
"@dreamstack-us/section-flow": "workspace:*"
}
}

Then run:

bun install

Live TypeScript Support

For live type resolution during development (no rebuild needed), ensure your app's tsconfig.json includes:

{
"compilerOptions": {
"customConditions": ["@dreamstack-us/source"]
}
}

This makes TypeScript resolve directly to .ts source files within the monorepo.

Standalone Installation (npm)

# When published to npm
npm install @dreamstack-us/section-flow

# or
bun add @dreamstack-us/section-flow

Quick Start

Basic Example

import { SectionFlow, type Section } from '@dreamstack-us/section-flow';

interface Item {
id: string;
title: string;
}

const sections: Section<Item>[] = [
{
key: 'featured',
title: 'Featured',
data: [
{ id: '1', title: 'Item One' },
{ id: '2', title: 'Item Two' },
],
},
{
key: 'recent',
title: 'Recent',
data: [
{ id: '3', title: 'Item Three' },
{ id: '4', title: 'Item Four' },
],
},
];

function MyList() {
return (
<SectionFlow<Item>
sections={sections}
renderItem={({ item }) => (
<View style={styles.item}>
<Text>{item.title}</Text>
</View>
)}
renderSectionHeader={({ section }) => (
<View style={styles.header}>
<Text style={styles.headerText}>{section.title}</Text>
</View>
)}
stickySectionHeadersEnabled={true}
/>
);
}

With Ref Methods

import { useRef } from 'react';
import { SectionFlow, type SectionFlowRef } from '@dreamstack-us/section-flow';

function MyList() {
const listRef = useRef<SectionFlowRef<Item>>(null);

const scrollToFeatured = () => {
listRef.current?.scrollToSection({
sectionKey: 'featured',
animated: true
});
};

const scrollToItem = () => {
listRef.current?.scrollToItem({
sectionKey: 'recent',
itemIndex: 2,
animated: true
});
};

return (
<>
<Button title="Go to Featured" onPress={scrollToFeatured} />
<SectionFlow<Item>
ref={listRef}
sections={sections}
renderItem={({ item }) => <ItemRow item={item} />}
renderSectionHeader={({ section }) => <Header section={section} />}
/>
</>
);
}

Core Concepts

Cell Recycling

Unlike React Native's SectionList which destroys and recreates components as they scroll off-screen, SectionFlow recycles them:

Traditional (SectionList):
Scroll down → Destroy top items → Create new bottom items → GC pressure

SectionFlow:
Scroll down → Move top items to pool → Reuse for bottom items → No GC

This eliminates garbage collection pauses and provides smooth 60fps scrolling.

Type-Based Pools

If your list has different item types, use getItemType for optimized recycling:

<SectionFlow<Item>
sections={sections}
getItemType={(item) => {
if (item.hasImage) return 'image-item';
if (item.isPromo) return 'promo-item';
return 'default';
}}
renderItem={({ item }) => {
// Items of same type will reuse each other's views
if (item.hasImage) return <ImageItem item={item} />;
if (item.isPromo) return <PromoItem item={item} />;
return <DefaultItem item={item} />;
}}
/>

Absolute Positioning

SectionFlow uses absolute positioning for all items:

// Each cell is rendered as:
<View style={{
position: 'absolute',
top: computedY,
left: 0,
width: containerWidth,
}}>
{yourContent}
</View>

This enables:

  • Efficient recycling without DOM structure changes
  • Precise layout corrections
  • Proper sticky header behavior

API Reference

SectionFlowProps

PropTypeDefaultDescription
sectionsSection<T>[]requiredArray of sections with data
renderItem(info: RenderItemInfo<T>) => ReactElementrequiredRender function for items
renderSectionHeader(info: RenderSectionHeaderInfo<T>) => ReactElement-Render function for headers
renderSectionFooter(info: RenderSectionFooterInfo<T>) => ReactElement-Render function for footers
keyExtractor(item: T, index: number) => stringautoExtract unique key from item
getItemType(item: T, index: number, section: Section<T>) => string-Return item type for recycling
estimatedItemSizenumber50Estimated height of items
estimatedSectionHeaderSizenumber40Estimated height of headers
stickySectionHeadersEnabledbooleantrueEnable sticky headers
horizontalbooleanfalseHorizontal scrolling
drawDistancenumber250Overscan distance in pixels
maxItemsInRecyclePoolnumber10Max recycled items per type
onEndReached(info: { distanceFromEnd: number }) => void-Called when scroll near end
onEndReachedThresholdnumber0.5How far from end to trigger
viewabilityConfigViewabilityConfig-Configure viewability tracking
onViewableItemsChanged(info) => void-Called when visible items change
ListHeaderComponentComponentType | ReactElement-Header above all sections
ListFooterComponentComponentType | ReactElement-Footer below all sections
ListEmptyComponentComponentType | ReactElement-Shown when sections empty
refreshingboolean-Show refresh indicator
onRefresh() => void-Pull-to-refresh callback
extraDataunknown-Trigger re-render on change
debugbooleanfalseShow debug overlays

SectionFlowRef Methods

interface SectionFlowRef<T> {
// Scroll to a section header
scrollToSection(options: {
sectionKey?: string;
sectionIndex?: number;
animated?: boolean;
viewPosition?: number; // 0=top, 0.5=center, 1=bottom
}): void;

// Scroll to a specific item
scrollToItem(options: {
sectionKey?: string;
sectionIndex?: number;
itemIndex: number;
animated?: boolean;
viewPosition?: number;
}): void;

// Scroll to offset
scrollToOffset(options: {
offset: number;
animated?: boolean
}): void;

// Scroll to end
scrollToEnd(options?: { animated?: boolean }): void;

// Toggle section collapse (Phase 2)
toggleSection(sectionKey: string): void;

// Get all section layouts
getSectionLayouts(): SectionLayoutInfo[];

// Get currently visible items
getVisibleItems(): ViewToken<T>[];

// Record user interaction (for viewability)
recordInteraction(): void;

// Flash scroll indicators
flashScrollIndicators(): void;
}

Section Type

interface Section<T> {
key: string; // Unique identifier
title?: string; // Display title
data: T[]; // Items in this section
}

RenderItemInfo

interface RenderItemInfo<T> {
item: T; // The item data
index: number; // Index within section
section: Section<T>; // Parent section
sectionIndex: number; // Index of section
}

ViewabilityConfig

interface ViewabilityConfig {
minimumViewTime?: number; // ms before considered "viewed" (default: 250)
viewAreaCoveragePercentThreshold?: number; // % of viewport item must cover
itemVisiblePercentThreshold?: number; // % of item that must be visible (default: 50)
waitForInteraction?: boolean; // Wait for user interaction first
}

Common Patterns

Alphabet Index (Contacts List)

function ContactsList() {
const listRef = useRef<SectionFlowRef<Contact>>(null);
const alphabet = sections.map(s => s.key);

return (
<View style={styles.container}>
<SectionFlow<Contact>
ref={listRef}
sections={sections}
renderItem={({ item }) => <ContactRow contact={item} />}
renderSectionHeader={({ section }) => (
<View style={styles.sectionHeader}>
<Text>{section.key}</Text>
</View>
)}
/>

{/* Alphabet index on the side */}
<View style={styles.alphabetIndex}>
{alphabet.map(letter => (
<Pressable
key={letter}
onPress={() => listRef.current?.scrollToSection({ sectionKey: letter })}
>
<Text style={styles.letter}>{letter}</Text>
</Pressable>
))}
</View>
</View>
);
}

Infinite Scroll

function InfiniteList() {
const [sections, setSections] = useState<Section<Item>[]>(initialSections);
const [loading, setLoading] = useState(false);

const loadMore = async () => {
if (loading) return;
setLoading(true);

const newItems = await fetchMoreItems();
setSections(prev => {
// Append to last section or create new
const updated = [...prev];
updated[updated.length - 1].data.push(...newItems);
return updated;
});

setLoading(false);
};

return (
<SectionFlow<Item>
sections={sections}
renderItem={({ item }) => <ItemRow item={item} />}
onEndReached={loadMore}
onEndReachedThreshold={0.5}
ListFooterComponent={loading ? <ActivityIndicator /> : null}
/>
);
}

Pull to Refresh

function RefreshableList() {
const [refreshing, setRefreshing] = useState(false);
const [sections, setSections] = useState<Section<Item>[]>([]);

const onRefresh = async () => {
setRefreshing(true);
const freshData = await fetchFreshData();
setSections(freshData);
setRefreshing(false);
};

return (
<SectionFlow<Item>
sections={sections}
renderItem={({ item }) => <ItemRow item={item} />}
refreshing={refreshing}
onRefresh={onRefresh}
/>
);
}

Viewability Tracking (Analytics)

function TrackedList() {
const handleViewableItemsChanged = useCallback(({ viewableItems, changed }) => {
// Track impressions
changed
.filter(item => item.isViewable)
.forEach(item => {
analytics.trackImpression(item.key);
});
}, []);

return (
<SectionFlow<Item>
sections={sections}
renderItem={({ item }) => <ItemRow item={item} />}
viewabilityConfig={{
minimumViewTime: 1000, // Must be visible for 1 second
itemVisiblePercentThreshold: 80, // 80% of item must be visible
}}
onViewableItemsChanged={handleViewableItemsChanged}
/>
);
}

Mixed Item Types

type ListItem =
| { type: 'product'; id: string; name: string; price: number }
| { type: 'ad'; id: string; imageUrl: string }
| { type: 'banner'; id: string; message: string };

function MixedList() {
return (
<SectionFlow<ListItem>
sections={sections}
getItemType={(item) => item.type}
renderItem={({ item }) => {
switch (item.type) {
case 'product':
return <ProductCard product={item} />;
case 'ad':
return <AdBanner ad={item} />;
case 'banner':
return <PromoBanner banner={item} />;
}
}}
/>
);
}

Performance Optimization

1. Use getItemType for Heterogeneous Lists

// ❌ Without getItemType - all items share one pool
<SectionFlow renderItem={({ item }) =>
item.large ? <LargeItem /> : <SmallItem />
} />

// ✅ With getItemType - separate pools, no layout thrashing
<SectionFlow
getItemType={(item) => item.large ? 'large' : 'small'}
renderItem={({ item }) =>
item.large ? <LargeItem /> : <SmallItem />
}
/>

2. Provide Accurate Size Estimates

// Measure your actual item heights and provide them
<SectionFlow
estimatedItemSize={72} // Your actual item height
estimatedSectionHeaderSize={48} // Your actual header height
renderItem={...}
/>

3. Memoize Render Functions

// ❌ Creates new function every render
<SectionFlow
renderItem={({ item }) => <ItemRow item={item} />}
/>

// ✅ Stable function reference
const renderItem = useCallback(
({ item }: RenderItemInfo<Item>) => <ItemRow item={item} />,
[]
);

<SectionFlow renderItem={renderItem} />

4. Memoize Item Components

// ❌ Re-renders on every parent render
function ItemRow({ item }: { item: Item }) {
return <View>...</View>;
}

// ✅ Only re-renders when item changes
const ItemRow = memo(function ItemRow({ item }: { item: Item }) {
return <View>...</View>;
});

5. Use extraData for External State

const [selectedId, setSelectedId] = useState<string | null>(null);

// extraData triggers re-render when selection changes
<SectionFlow
sections={sections}
extraData={selectedId}
renderItem={({ item }) => (
<ItemRow
item={item}
selected={item.id === selectedId}
onSelect={() => setSelectedId(item.id)}
/>
)}
/>

6. Adjust Draw Distance for Your Use Case

// Fast scrolling lists - increase overscan
<SectionFlow drawDistance={500} />

// Memory-constrained - reduce overscan
<SectionFlow drawDistance={100} />

Troubleshooting

Items Not Rendering

Cause: Missing or zero container height.

Solution: Ensure parent has defined height:

// ❌ No height defined
<View>
<SectionFlow sections={sections} />
</View>

// ✅ Flex or explicit height
<View style={{ flex: 1 }}>
<SectionFlow sections={sections} />
</View>

Sticky Headers Not Working

Cause: stickySectionHeadersEnabled is false or no renderSectionHeader.

Solution:

<SectionFlow
stickySectionHeadersEnabled={true} // Ensure this is true
renderSectionHeader={({ section }) => (
<View style={styles.header}>
<Text>{section.title}</Text>
</View>
)}
/>

Items Flickering During Fast Scroll

Cause: Not enough items in recycle pool.

Solution: Increase pool size:

<SectionFlow
maxItemsInRecyclePool={20}
drawDistance={400}
/>

TypeScript Generic Not Inferring

Cause: Missing explicit generic.

Solution: Explicitly provide the generic:

// ❌ Type might not infer correctly
<SectionFlow sections={sections} />

// ✅ Explicit generic
<SectionFlow<MyItemType> sections={sections} />

Performance Issues on Old Architecture

Cause: SectionFlow is optimized for New Architecture.

Solution: Ensure New Architecture is enabled:

// app.json (Expo)
{
"expo": {
"newArchEnabled": true
}
}

Migration from SectionList

Before (SectionList)

import { SectionList } from 'react-native';

<SectionList
sections={sections}
renderItem={({ item, index, section }) => <ItemRow item={item} />}
renderSectionHeader={({ section }) => <Header section={section} />}
keyExtractor={(item) => item.id}
stickySectionHeadersEnabled={true}
/>

After (SectionFlow)

import { SectionFlow } from '@dreamstack-us/section-flow';

<SectionFlow<ItemType>
sections={sections}
renderItem={({ item, index, section }) => <ItemRow item={item} />}
renderSectionHeader={({ section }) => <Header section={section} />}
keyExtractor={(item) => item.id}
stickySectionHeadersEnabled={true}

// NEW: Performance optimizations
estimatedItemSize={72}
getItemType={(item) => item.type}
/>

Key Differences

FeatureSectionListSectionFlow
Recycling❌ Virtualization✅ Cell recycling
Sticky HeadersBuggyWorks correctly
scrollToLocationOften failsReliable
TypeScriptPoor genericsFull generics
ArchitectureBothNew Arch only
Performance~30fps on Android60fps

Questions?


Last updated: January 2026