React Native
How do I add infinite scrolling to a list in React Native?
Fetch the next page in the list's onEndReached callback, and set onEndReachedThreshold to control how early it runs. Track loading and whether more pages exist in refs so the same page isn't requested twice, and show a spinner in ListFooterComponent. FlatList and FlashList use the same props.
Updated October 31, 2026
Infinite scroll sounds like a big feature, but it’s really a list that loads the next page when someone gets close to the bottom. FlatList and FlashList both have what you need built in, so most of the work is making sure each page gets requested exactly once.
How it works
Both lists call onEndReached when the scroll position gets within onEndReachedThreshold of the end of the content. The threshold is measured in screen lengths of the list, so 0.5 means half a visible list away from the bottom, which gives the next page time to arrive before anyone hits the end. We’d set it explicitly rather than rely on the default.
onEndReached runs once for a given content height, and runs again when that height changes. A first page too short to fill the screen puts the end in range right away, and a loading spinner at the bottom changes the height, so the callback can run again before your first request has finished. That’s why the guard in step 2 uses a ref, which changes the moment you set it, while state only changes on the next render.
How to add it
-
Write a function that fetches one page. With Supabase,
range()takes 0-based, inclusive offsets, and it needs anorder()so pages don’t shift between requests:const PAGE_SIZE = 20; async function fetchPage(page: number) { const from = page * PAGE_SIZE; const { data, error } = await supabase .from("posts") .select("id, title") .order("created_at", { ascending: false }) .range(from, from + PAGE_SIZE - 1); if (error) throw error; return data; } -
Load more from
onEndReached, guarded by refs:import { useCallback, useEffect, useRef, useState } from "react"; import { ActivityIndicator, FlatList, Text } from "react-native"; export function Feed() { const [items, setItems] = useState<{ id: string; title: string }[]>([]); const [loading, setLoading] = useState(false); const busy = useRef(false); const hasMore = useRef(true); const page = useRef(0); const loadMore = useCallback(async () => { if (busy.current || !hasMore.current) return; busy.current = true; setLoading(true); try { const next = await fetchPage(page.current); page.current += 1; hasMore.current = next.length === PAGE_SIZE; setItems((current) => [...current, ...next]); } finally { busy.current = false; setLoading(false); } }, []); useEffect(() => { loadMore(); }, [loadMore]); return ( <FlatList data={items} keyExtractor={(item) => item.id} renderItem={({ item }) => <Text>{item.title}</Text>} onEndReached={loadMore} onEndReachedThreshold={0.5} ListFooterComponent={loading ? <ActivityIndicator style={{ margin: 16 }} /> : null} /> ); } -
Switch to FlashList if you like by installing it with
npx expo install @shopify/flash-listand changingFlatListtoFlashListfrom@shopify/flash-list. The props above work unchanged. On Expo SDK 55, that command installs FlashList 2, which no longer usesestimatedItemSize, so drop that prop if older code passes it. -
Use a cursor instead of page numbers when new rows arrive often. Ask for rows older than the last item you have, in the same order, for example with
.lt("created_at", lastItem.created_at).limit(PAGE_SIZE)instead ofrange(), so new posts at the top can’t push an item onto the next page twice.
How you know it works
When you scroll to the bottom, a spinner shows briefly while the next 20 items load. Your network log should show one request per page with no repeats, and once you reach the last item, the requests stop.
If it loads twice or not at all
If pages repeat, check that the guard is a ref and that keyExtractor returns a unique id. If more items never load, check that the list isn’t inside a ScrollView (nesting it there triggers a React Native warning that it can break windowing and other list behavior), and give it a parent with flex: 1. And if a failed request stops the list for good, show a retry button in the footer that calls loadMore again.
Ask the agent
If your app is in Draftbit, paste this into the agent, and it can read your code and make the change for you.
Make my feed list load 20 items at a time and fetch the next 20 when someone scrolls near the bottom. Show a spinner while it loads, never load the same page twice, and stop when there are no more items.