Lists Best Practices
A condensed summary of the 25 most important best practices drawn from every page in this section.
Search across all documentation pages
A condensed summary of the 25 most important best practices drawn from every page in this section.
Choose ScrollView for small static content: Fewer than ~20 non-growing items with mixed layout types - once the list grows or paginates, switch to virtualization.
Default to FlatList for feeds: FlatList recycles rows off-screen - mapping hundreds of items inside ScrollView allocates every row in memory and blocks the JS thread.
Evaluate FlashList for heavy feeds: On SDK 57, Shopify FlashList improves recycling throughput when profiles show frame drops - migrate with estimatedItemSize and a measured migration checklist.
Keys must be stable strings: keyExtractor should return server ids - index keys break selection, animation, and state when items sort, filter, or paginate.
Never key on array index for dynamic data: key={index} causes wrong row content to flash during insert/delete - users see likes jump between posts.
Extract renderItem to a memoized row: Inline renderItem closures recreate every parent render - const Row = memo(function Row…) plus stable useCallback handler.
Pass extraData when selection drives UI: Without extraData, FlatList may skip re-rendering visible rows after selectedId changes - pass the minimal state slice that affects row appearance.
Hoist list styles to StyleSheet: Fresh contentContainerStyle={{ padding: 16 }} on every parent render invalidates list optimizations - cache wrapper styles.
Use getItemLayout for fixed-height rows: Inbox and settings rows with uniform height skip async measurement - scrolling to index becomes instant.
Do not lie to getItemLayout: Wrong length or offset produces blank gaps and jumpy scroll - measure once in design review and keep height in sync with separators.
Separate empty and loading states: ListEmptyComponent should not spin forever - branch ListEmptyComponent vs ListFooterComponent skeletons while the first page loads.
Pull-to-refresh resets pagination: onRefresh should clear cursor state and refetch page one - appending to a stale cursor after refresh duplicates items.
Guard onEndReached: Fire only when !isFetching && hasNextPage - otherwise momentum scroll triggers duplicate page fetches at the bottom.
Show footer skeletons for pagination: A small loading row at ListFooterComponent beats a full-screen spinner for page two - users keep context on the feed.
Precompute SectionList sections in useMemo: Grouping hundreds of items in render allocates every pass - derive sections when source data or sort order changes.
Use sticky headers intentionally: stickySectionHeadersEnabled helps alphabet lists - disable when headers overlap complex hero content at the top.
Enable nestedScrollEnabled for horizontal rails: Vertical feed + horizontal shelf needs explicit nested scroll on Android - test gesture handoff on real devices.
Avoid vertical FlatList inside vertical ScrollView: Double scroll containers fight gestures and break virtualization - one axis should own scrolling; use horizontal inner lists only.
Tune windowSize after profiling: Lower windowSize reduces memory; too low causes blank rows during fast flings - start with defaults, adjust with Flashlight or RN DevTools.
Set FlashList estimatedItemSize close to reality: Estimates far from measured height cause layout thrash and blank frames - measure average row height from production content.
Avoid anonymous functions in renderItem: onPress={() => toggle(item.id)} inside rows defeats memo - pass id to rows that call stable handlers.
Validate dynamic row heights: Variable-height rows without measurement cause virtualization to mount wrong content - use FlashList overrideItemLayout or accept non-virtualized sections.
Test rotation and font scaling on lists: Larger accessibility fonts change row height - fixed getItemLayout may need dynamic measurement or updated constants.
Scroll to top on tab re-tap via ref: Expose flatListRef.current?.scrollToOffset({ offset: 0 }) from navigation events - feed apps expect this behavior.
Run the virtualization gotchas checklist before release: Walk keys, heights, empty states, and nested scroll cases on a low-end Android device - simulators hide blank-row bugs.
FlatList until profiling proves jank on target devices.getItemLayout accuracy, increase windowSize slightly, or improve FlashList estimatedItemSize - blanks are almost always height math, not "React being slow."Stack versions: This page was written for React 19.2.3, React Native 0.86.0, and Expo SDK 57 (
expo~57.0.4).
Reviewed by Chris St. John·Last updated Jul 16, 2026