Scrolling through 10,000 products on a mid-range Android phone is still one of the most brutal stress tests in React Native. I've watched FlatList buckle under this exact scenario more times than I'd like to admit — blank cells flashing past, frames dropping on anything with a thumbnail, the JS thread pinned at 100%. It's been the default answer for years, sure, but its virtualization approach just can't keep up with the New Architecture. That's the gap FlashList v2 from Shopify was rebuilt to close.
FlashList v2 shipped as a production-ready major release in late 2025, and it's now the recommended list component for any modern React Native app running on the New Architecture. In Expo SDK 55 — where the New Architecture is permanently on — FlashList v2 is effectively a first-class citizen.
So, let's dive in. This guide walks through everything you need to ship high-performance lists in 2026: installation, basic usage, the v1 to v2 migration, new hooks, masonry layouts, sticky headers, and some realistic performance tips I've picked up along the way.
Why FlashList v2 Exists
FlatList uses virtualization — it mounts and unmounts rows as they scroll in and out of the viewport. Sounds efficient on paper. In practice, mounting a complex row on a low-end device can easily cost 16ms or more, which is one dropped frame. The result? Blank white cells, laggy scrolls, and a JS thread that's wheezing.
FlashList takes a different route: cell recycling. Instead of tearing down and recreating components, it keeps a small, fixed pool of views alive and rebinds them with new data as you scroll. Shopify's internal benchmarks already showed v1 delivering dramatic improvements, but it came with friction — developers had to hand-tune estimatedItemSize, rely on native code, and fight with horizontal nesting.
Honestly, estimatedItemSize was my biggest personal gripe with v1. Get it wrong and you'd see scroll jumps. Get it right and it still felt fragile. v2 rips that whole concern out.
Here's what changed in v2:
- JS-only implementation — no native modules, no autolinking headaches, much easier to maintain.
- No size estimates required — the biggest ergonomic win, hands down.
estimatedItemSizeis gone. - Pixel-perfect scrolling via a progressive refinement algorithm that continuously corrects scroll position as items measure.
- Adaptive render windows that factor in scroll velocity and direction instead of a fixed lookahead.
- Up to 50% less blank area during fast scrolling compared to v1.
- New hooks —
useRecyclingState,useLayoutState,useMappingHelper, anduseFlashListContext. - Better horizontal lists that auto-size and coordinate when nested inside a vertical FlashList.
One hard requirement to flag upfront: FlashList v2 only runs on React Native's New Architecture. If you're still on the legacy architecture, you'll either stay on v1.x or bite the bullet and upgrade. In Expo, that means SDK 54 or newer. SDK 55 makes the New Architecture mandatory, so the decision is already made for you.
Installing FlashList v2 in an Expo App
Install into any Expo SDK 54+ project using expo install, which will pin a compatible version for you:
npx expo install @shopify/flash-list
If you explicitly want v2 (which I'd recommend for any new app), force the major:
npm install @shopify/flash-list@^2.0.0
FlashList v2 has no native dependencies, so you do not need to run npx expo prebuild or rebuild your dev client just to add it. It works out of the box with Expo Go on SDK 55. (This alone saves me about 3 minutes every time I spin up a new project — small thing, but it adds up.)
Confirm the New Architecture is on in your app.json (for SDK 54 — SDK 55 enforces it automatically):
{
"expo": {
"newArchEnabled": true
}
}
Your First FlashList: A Drop-In FlatList Replacement
The nicest thing about FlashList is that its API mirrors FlatList so closely that most migrations end up being a one-line import swap. Here's a complete Expo screen rendering 5,000 users:
import { FlashList } from "@shopify/flash-list";
import { Text, View, StyleSheet } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
type User = { id: string; name: string; email: string };
const USERS: User[] = Array.from({ length: 5000 }, (_, i) => ({
id: String(i),
name: `User ${i}`,
email: `user${i}@example.com`,
}));
export default function UsersScreen() {
return (
<SafeAreaView style={{ flex: 1 }} edges={["top"]}>
<FlashList
data={USERS}
keyExtractor={(item) => item.id}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.name}>{item.name}</Text>
<Text style={styles.email}>{item.email}</Text>
</View>
)}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
row: { paddingVertical: 12, paddingHorizontal: 16, borderBottomWidth: 0.5, borderBottomColor: "#e5e5e5" },
name: { fontSize: 16, fontWeight: "600" },
email: { fontSize: 13, color: "#666" },
});
Notice what's not there: no estimatedItemSize, no getItemLayout, no removeClippedSubviews, no windowSize. v2 works all of that out dynamically.
One best practice worth emphasizing: always pass a stable keyExtractor. The docs call this "highly recommended" in v2, because stable keys let the recycling algorithm avoid layout glitches when users scroll upward after items have already been recycled. I've shipped code without one and gotten away with it — until I didn't. Just set it.
Migrating from FlashList v1 to v2
If you're already shipping FlashList v1, migration is a contained, mostly mechanical change. Below are the breaking changes, each with the v1 and v2 equivalents side by side.
1. Remove estimatedItemSize and Friends
v1 lived on estimates. v2 computes everything itself. Just delete these props entirely:
estimatedItemSizeestimatedListSizeestimatedFirstItemOffsetonBlankAreadisableHorizontalListHeightMeasurementdisableAutoLayout
// v1
<FlashList
data={items}
renderItem={renderItem}
estimatedItemSize={80}
estimatedListSize={{ height: 800, width: 375 }}
/>
// v2
<FlashList data={items} renderItem={renderItem} />
2. Use the masonry Prop Instead of MasonryFlashList
The separate MasonryFlashList component is deprecated. Pass masonry directly to FlashList:
// v1
import { MasonryFlashList } from "@shopify/flash-list";
<MasonryFlashList data={photos} numColumns={2} renderItem={renderPhoto} />
// v2
import { FlashList } from "@shopify/flash-list";
<FlashList
data={photos}
masonry
numColumns={2}
optimizeItemArrangement
renderItem={renderPhoto}
/>
The new optimizeItemArrangement prop subtly reorders items so columns stay balanced. Particularly nice for photo grids, where raw item order can produce really uneven column heights.
3. overrideItemLayout Only Sets span
In v2, overrideItemLayout no longer accepts layout.size, because FlashList measures everything itself now. Use it only to change how many columns an item spans:
<FlashList
data={items}
masonry
numColumns={3}
overrideItemLayout={(layout, item) => {
layout.span = item.featured ? 3 : 1; // full-width featured items
}}
renderItem={renderItem}
/>
4. Ref Type Renamed
The imperative ref type moved from FlashList to FlashListRef:
// v1
const listRef = useRef<FlashList<User>>(null);
// v2
import { FlashList, FlashListRef } from "@shopify/flash-list";
const listRef = useRef<FlashListRef<User>>(null);
5. Replace CellContainer with View
If you imported CellContainer in v1, just drop it and use a plain View from react-native. The container role is handled internally now.
6. Memoize Row Components
v2 is stricter about re-renders. If you previously leaned on v1's selective updates, wrap your row component in React.memo and memoize any inline callbacks passed into renderItem. This is also where React Compiler (enabled by default in SDK 54+) helps enormously — it auto-memoizes most of this for you, which is genuinely one of the best quality-of-life upgrades of the year.
New Hooks in FlashList v2
Recycled components come with a subtle trap: local useState leaks across items because the component instance is reused. This one caught me out the first time I built a list with an "expanded" toggle in v1 — scroll down, scroll back up, and suddenly three other rows are also expanded. v2 ships dedicated hooks that are recycling-aware to fix exactly this.
useRecyclingState
Behaves like useState but resets automatically when the row is recycled with a new item. Pass a dependency array and an optional reset callback:
import { useRecyclingState } from "@shopify/flash-list";
import { Pressable, Text, View } from "react-native";
function ProductRow({ item }: { item: Product }) {
const [expanded, setExpanded] = useRecyclingState(
false,
[item.id],
() => {
// Optional: runs synchronously on reset, e.g. clear a nested scroll.
}
);
return (
<Pressable onPress={() => setExpanded((v) => !v)}>
<View style={{ padding: 16 }}>
<Text>{item.title}</Text>
{expanded && <Text>{item.description}</Text>}
</View>
</Pressable>
);
}
useLayoutState
Use this when a state change also changes the item's size. It tells FlashList to remeasure the cell so the scroll offset stays correct:
import { useLayoutState } from "@shopify/flash-list";
function CollapsibleRow({ item }) {
const [open, setOpen] = useLayoutState(false);
return (
<Pressable onPress={() => setOpen((v) => !v)}>
<View style={{ height: open ? 180 : 56 }}>
<Text>{item.title}</Text>
{open && <Text>{item.body}</Text>}
</View>
</Pressable>
);
}
useMappingHelper and useFlashListContext
useMappingHelper generates optimized keys when you need to .map() inside a row. useFlashListContext exposes the list's scroll view ref and imperative handles from a descendant component — really useful for floating action buttons that need to scroll-to-top from deep inside the tree.
Multiple Item Types and Recycling Pools
If your list mixes cards, banners, and section headers, you'll want to tell FlashList to keep separate recycling pools with getItemType. This avoids "cross-type" recycles, where a banner layout gets reused for a card (and looks weird for a split second):
type FeedItem =
| { type: "post"; id: string; body: string }
| { type: "ad"; id: string; imageUrl: string }
| { type: "header"; id: string; title: string };
<FlashList<FeedItem>
data={feed}
getItemType={(item) => item.type}
renderItem={({ item }) => {
switch (item.type) {
case "post":
return <PostRow item={item} />;
case "ad":
return <AdRow item={item} />;
case "header":
return <SectionHeader item={item} />;
}
}}
/>
Sticky Headers and Horizontal Lists
Sticky headers in v2 use an Animated implementation, so the transient gap you sometimes saw between sticky rows during fast scrolling is gone. Use stickyHeaderIndices exactly as you would with SectionList or v1:
<FlashList
data={data}
renderItem={renderItem}
stickyHeaderIndices={stickyIndices}
/>
Sticky headers only work on vertical lists. For horizontal lists, just remove stickyHeaderIndices entirely.
Horizontal lists are one of v2's standout improvements, honestly. Nested horizontal FlashLists inside a vertical FlashList now coordinate their layout — the vertical parent waits for the child layout to finish, which eliminates the jumpy "shelf" effect that used to plague home screens with carousels. The only caveat: make sure the outer vertical list is also a FlashList for this coordination to kick in. Mix in a ScrollView or FlatList as the parent and the magic doesn't happen.
Performance Tips That Actually Matter in v2
- Memoize your row component. Wrap it in
React.memo, or let React Compiler do it for you. Without memoization, every list-level state change re-renders all visible rows. (Ask me how I know.) - Use
getItemTypefor heterogeneous lists. Keeps recycling pools clean and dramatically reduces layout thrash. - Avoid inline functions for
onPress. UseuseCallbackor push handlers into the row component itself. - Preload images off-list. Use
expo-imagewithImage.prefetchfor lists that scroll into data-heavy content. - Don't nest
ScrollViewinside FlashList. UseListHeaderComponentor a nested horizontalFlashListinstead. - Enable Hermes. Default in Expo SDK 55, but double-check on bare workflows — FlashList's recycling model benefits noticeably from Hermes' fast object creation.
FlashList v2 vs FlatList: A Straightforward Comparison
| Aspect | FlatList | FlashList v2 |
|---|---|---|
| Rendering strategy | Virtualization (mount/unmount) | Cell recycling (reuse) |
| Architecture | Legacy and New | New Architecture only |
| Native code | Required | JS-only |
| Size estimates | N/A | Not needed (auto-measured) |
| Blank cells during scroll | Common on complex rows | Up to 50% less than v1, minimal in practice |
| Sticky headers | Yes, occasional jitter | Animated, smooth |
| Masonry | Not built-in | First-class via masonry prop |
| Expo Go compatibility | Yes | Yes (SDK 55+) |
| Best for | Small, simple lists, legacy architecture | Complex, long lists on modern apps |
My rule of thumb for 2026: if you're on SDK 55 or higher, default to FlashList v2. Fall back to FlatList only if you're still shipping to the legacy architecture, or if the list is trivially short (fewer than ~30 fixed-height items).
Putting It All Together: A Real-World Feed
Here's a compact but realistic feed screen that combines a handful of the v2 concepts — typed items, getItemType, masonry-friendly media sections, and an image-heavy row using expo-image:
import { FlashList } from "@shopify/flash-list";
import { Image } from "expo-image";
import { Text, View, StyleSheet } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
type FeedItem =
| { type: "post"; id: string; author: string; body: string }
| { type: "photo"; id: string; uri: string; height: number };
export default function Feed({ items }: { items: FeedItem[] }) {
return (
<SafeAreaView style={{ flex: 1 }} edges={["top"]}>
<FlashList<FeedItem>
data={items}
keyExtractor={(item) => item.id}
getItemType={(item) => item.type}
renderItem={({ item }) => {
if (item.type === "post") {
return (
<View style={styles.post}>
<Text style={styles.author}>{item.author}</Text>
<Text>{item.body}</Text>
</View>
);
}
return (
<Image
source={{ uri: item.uri }}
style={{ width: "100%", height: item.height }}
contentFit="cover"
transition={150}
/>
);
}}
/>
</SafeAreaView>
);
}
const styles = StyleSheet.create({
post: { padding: 16, borderBottomWidth: 0.5, borderBottomColor: "#e5e5e5" },
author: { fontWeight: "600", marginBottom: 4 },
});
Common Pitfalls and How to Fix Them
- "
FlashList is trying to render old architecture" error — upgrade to Expo SDK 54+ or setnewArchEnabled: true. - State leaks across rows — switch
useStatetouseRecyclingStatefor any item-specific state. - Rows re-render on every scroll — wrap the row component in
React.memoand memoize therenderItemcallback. - Horizontal list jumps on mount — make sure the outer container is also a
FlashListso child-layout coordination kicks in. - Missing image heights in masonry — provide height via server, CDN metadata, or a quick
Image.getSizecall. v2 no longer estimates for you.
FAQ
Is FlashList v2 backward compatible with v1?
No. v2 is a breaking release — it only runs on the New Architecture, estimatedItemSize and several other props are removed, and MasonryFlashList is replaced by the masonry prop. If you can't migrate to the New Architecture yet, pin @shopify/flash-list@^1.7 and revisit later.
Does FlashList v2 work with Expo Go?
Yes. Because v2 is a JS-only library with no native dependencies, it runs in Expo Go on SDK 55 and later without a custom dev client or expo prebuild.
Should I replace every FlatList with FlashList?
Not necessarily. For lists with a handful of items and simple row layouts, FlatList is perfectly fine and avoids an extra dependency. Reach for FlashList when you have hundreds or thousands of rows, complex row components, horizontal carousels nested in vertical lists, or when you measure dropped frames while scrolling.
Do I still need estimatedItemSize in v2?
No. v2 measures items automatically via its progressive refinement algorithm. You can delete estimatedItemSize, estimatedListSize, and estimatedFirstItemOffset from your code without a second thought.
Can FlashList v2 replace SectionList?
Yes, in most cases. Flatten your sections into a single array, mark section headers with a type field, use getItemType to keep recycling pools clean, and pass stickyHeaderIndices for the sticky effect. You'll get better performance than SectionList for long feeds — in my experience, noticeably so past a few hundred items.
Wrapping Up
FlashList v2 is one of those rare upgrades where the library does more while asking for less from you — no estimates, no native modules, and a cleaner API. Paired with Expo SDK 55's mandatory New Architecture and the React Compiler that ships with it, you can build a 10,000-row feed in 2026 that scrolls at 60 FPS on devices that would've choked on the same data two years ago.
If you only take one thing from this guide: on any new Expo project, start with FlashList, keep a stable keyExtractor, memoize your rows, and reach for useRecyclingState the moment a row has its own interactive state. Everything else is optimization on top of an already fast foundation.