You're building a screen in Rork that has a banner at the top and a long list of articles below it. Everything looks fine — until you open the dev console and see this in red:
VirtualizedLists should never be nested inside plain ScrollViews
with the same orientation because it can break windowing and other
functionality - use another VirtualizedList-backed container instead.
I've run into this many times across the apps I've shipped with Rork. It's tempting to ignore — the screen still works, after all — but the moment your list grows past a few hundred rows, scrolling gets choppy and memory usage spikes. This guide walks through the patterns I actually reach for when I see this warning in a Rork-generated React Native project.
Why React Native Yells at You Here
FlatList and SectionList are both built on top of VirtualizedList. That's the engine that only renders what's currently on screen and recycles rows as you scroll. It's the reason you can render thousands of items without your phone catching fire.
The trouble is, when you put that engine inside a regular ScrollView with the same scroll axis, the outer ScrollView asks every child to fully expand so it can measure the total height. That completely defeats the virtualization — the inner list is forced to render every single row at once.
The way I picture it: imagine putting a paper-towel dispenser inside another paper-towel dispenser. Neither of them can roll properly anymore. Only one component should own the scrolling on any given axis.
Fix 1: Move Your Header Into the FlatList Itself (My Default)
The cleanest fix is to throw out the wrapping ScrollView and let FlatList carry the whole screen. Move whatever you wanted at the top into ListHeaderComponent, and any footer content into ListFooterComponent.
import { FlatList, View, Text, Image, StyleSheet } from "react-native";
type Article = { id: string; title: string; excerpt: string };
export default function FeedScreen({ articles }: { articles: Article[] }) {
// Anything you would have put above the list goes here.
// No more nested ScrollView, no more warning.
const Header = (
<View style={styles.header}>
<Image source={require("./assets/banner.png")} style={styles.banner} />
<Text style={styles.heading}>Latest articles</Text>
</View>
);
return (
<FlatList
data={articles}
keyExtractor={(item) => item.id}
ListHeaderComponent={Header}
ListFooterComponent={<View style={{ height: 32 }} />}
renderItem={({ item }) => (
<View style={styles.row}>
<Text style={styles.title}>{item.title}</Text>
<Text style={styles.excerpt} numberOfLines={2}>
{item.excerpt}
</Text>
</View>
)}
/>
);
}
const styles = StyleSheet.create({
header: { padding: 16, gap: 12 },
banner: { width: "100%", height: 140, borderRadius: 12 },
heading: { fontSize: 20, fontWeight: "700" },
row: { padding: 16, borderBottomWidth: 1, borderBottomColor: "#eee" },
title: { fontSize: 16, fontWeight: "600", marginBottom: 4 },
excerpt: { color: "#555" },
});Not only does the warning go away, the header and footer benefit from the same recycling logic the rows do, so scroll stays smooth even as the list grows. When asking Rork to refactor your screen, be specific: "Remove the wrapping ScrollView and move the banner into ListHeaderComponent of the FlatList." That phrasing gives you clean output the first time around.
Fix 2: Horizontal Inside Vertical Is Allowed
Read the warning carefully — it says "with the same orientation." A vertical ScrollView containing a horizontal FlatList is perfectly fine. That's exactly the pattern you want for a home screen with horizontal carousels stacked vertically.
<ScrollView>
<Text style={styles.section}>Featured</Text>
<FlatList
data={featured}
horizontal
showsHorizontalScrollIndicator={false}
keyExtractor={(item) => item.id}
renderItem={({ item }) => <FeatureCard item={item} />}
/>
<Text style={styles.section}>Latest</Text>
{/* If you also want a vertical list here, move it into ListHeaderComponent. */}
</ScrollView>Most "shelf-style" home layouts can be expressed this way without any nesting issues.
Fix 3: Reach for SectionList When You Have Groups
If your content naturally splits into groups — "This week's pick," "Latest," "Most read" — SectionList is a great fit. It keeps virtualization on while letting you express structure cleanly.
import { SectionList, Text, View } from "react-native";
const sections = [
{ title: "This week's pick", data: weeklyArticles },
{ title: "Latest", data: latestArticles },
];
<SectionList
sections={sections}
keyExtractor={(item, index) => item.id + index}
renderSectionHeader={({ section: { title } }) => (
<Text style={styles.sectionTitle}>{title}</Text>
)}
renderItem={({ item }) => <ArticleRow item={item} />}
ListHeaderComponent={<HomeBanner />}
stickySectionHeadersEnabled
/>Turning on stickySectionHeadersEnabled is a small touch that significantly improves the feel on long screens — readers always know which section they're in.
Fix 4: Drop In FlashList for Heavy Lists
Once your list crosses several hundred rows, consider replacing FlatList with @shopify/flash-list from Shopify. The API is almost identical, but the cell-recycling implementation is much smarter, so frame drops on fast scroll are dramatically reduced.
import { FlashList } from "@shopify/flash-list";
<FlashList
data={articles}
estimatedItemSize={88} // Always set this — it's required for performance
keyExtractor={(item) => item.id}
ListHeaderComponent={<HomeHeader />}
renderItem={({ item }) => <ArticleRow item={item} />}
/>estimatedItemSize is the one thing you must set — it's how FlashList pre-allocates cells. Rork tends to scaffold with FlatList by default, so I treat the FlashList migration as a "scale-up" step once a screen starts showing perceptible jank.
Anti-Patterns That Just Hide the Problem
When you're tired of seeing the warning, it's tempting to "fix" it by setting nestedScrollEnabled on the ScrollView, or worse, putting scrollEnabled={false} on the FlatList and letting the outer container scroll everything. Don't.
That second pattern in particular forces the list to render every row up front, which silently turns into a memory bomb on lower-end devices. I learned this the hard way once on a TestFlight build — fine on my iPhone, instant crash on a friend's older Android. Pick one of the four fixes above and stick with it.
There's also a third anti-pattern I see often: wrapping a tall list in <View style={{ flex: 1, height: 800 }}> to "force the height" so the warning goes away. The warning doesn't actually disappear — it's just hidden because the container size is now finite. The virtualization is still broken; you've only postponed the crash by giving it a hard ceiling. If you find yourself reaching for an explicit pixel height around a list, that's a strong signal to step back and apply Fix 1 instead.
One more nuance worth knowing: if you're using a navigation library like React Navigation, the screen container itself is already a View with flex: 1. You don't need to add another ScrollView on top to make the screen scrollable. A bare FlatList placed directly inside the screen will scroll just fine. This trips up a lot of newcomers because Rork's AI sometimes scaffolds a ScrollView even when one isn't needed.
A Couple of Subtle Cases People Miss
Two patterns trip people up even after they understand the basic rule.
The first is a FlatList inside a KeyboardAvoidingView inside a ScrollView. It's an easy stack to end up with on a form screen that also shows a list of suggestions. The fix is the same as Fix 1: drop the outer ScrollView and let the FlatList own the scroll, with the form fields living inside ListHeaderComponent. KeyboardAvoidingView plays nicely with a FlatList directly, so you lose nothing.
The second is a Modal containing a ScrollView that wraps a FlatList. Modals are easy to forget about because they feel like a separate world, but the same warning applies inside them. Treat the modal body as just another screen and apply Fix 1 there too.
If the warning still appears after you've cleaned up the outer ScrollView, do a quick grep for any reusable wrapper components in your project — sometimes the outer ScrollView is hiding inside a <Screen> or <Container> component you wrote weeks ago. The fastest way I've found to track this down is to temporarily add <View style={{ flex: 1, borderWidth: 4, borderColor: "red" }}> around suspect components and watch which one is wrapping your list.
How I Decide Between These Four
When I'm in the middle of building a screen, this is the order I run through:
- The list is the main thing on the screen → Fix 1 (FlatList + ListHeaderComponent)
- A vertical screen with horizontal carousels → Fix 2 (horizontal FlatList nested is fine)
- Distinct, named groupings → Fix 3 (SectionList)
- Hundreds or thousands of rows, performance is a concern → Fix 4 (FlashList)
If you're not sure, start with Fix 1. Most of the time, just removing the outer ScrollView cleans up the code and the warning at the same time.
If you want to deepen your understanding of how React Native components fit together inside a Rork-generated project, the Reading Rork-Generated Code: A React Native Primer is a good companion read. And if your scrolling pain is more about input fields hiding behind the keyboard than list virtualization, the Rork Keyboard Avoiding Input Overlap Fix Guide covers that side of the story.
Pick One Screen and Convert It
You don't have to fix every screen at once. What I do is grep my project for <ScrollView> blocks that wrap a FlatList, pick the worst offender — usually the home or feed screen — and migrate just that one to Fix 1. The smoother scroll on a low-end device is the moment when the warning's reasoning truly clicks.
That single conversion sets the template for every list-heavy screen you'll build next in Rork.