RORK LABJP
TOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the roundTOOLING — Rork's developer repos keep moving: rork-xcode was updated on July 16, rork-device on July 15, and rork-plist on July 13OPUS46 — Claude Opus 4.6 is live in Rork, and Rork Max is built to assemble apps on top of Claude CodeSIM — A cloud iOS simulator runs in the browser, with one click to install on a device and two clicks to publish to the App StoreMAX — Rork Max emits pure Swift rather than React Native, reaching iPhone, iPad, Apple Watch, Apple TV, Vision Pro, and even iMessageNATIVE — That opens up HealthKit, ARKit and LiDAR, NFC, Dynamic Island, Live Activities, 3D through Metal, and on-device inference with Core MLSEED — Rork raised a $15M seed led by Left Lane Capital, with Peak XV and a16z Speedrun joining the round
Articles/Dev Tools
Dev Tools/2026-05-22Intermediate

Why FlatList's onEndReached Fires Multiple Times — and How to Stop It

After wiring up infinite scroll in a Rork-generated FlatList, you may notice the same paginated request hitting your API two or three times in a row. Here's why onEndReached fires more often than you expect and how to add a two-layer defense that survives production.

FlatList9onEndReachedReact Native209Rork515PaginationTroubleshooting38Performance23

Why FlatList's onEndReached Fires Multiple Times — and How to Stop It

You finish wiring up infinite scroll, open the API log to double-check, and see "fetch page 2", "fetch page 3", "fetch page 3", "fetch page 3" stacked on top of each other. If you ship the Rork-generated FlatList code as-is to a real device, onEndReached is very likely to fire several times during the initial render and again when the screen comes back into focus. Left alone, this duplicates server records and burns through free API quotas surprisingly fast.

As an indie developer running a portfolio of wallpaper apps that has accumulated around 50 million downloads since 2014, I hit exactly this problem when I migrated a thumbnail grid from FlatList to FlashList. The list interleaves AdMob native ads every eight items, so each spurious load also inflated duplicated impressions — a problem that hurts revenue as much as it hurts the server bill.

Why onEndReached is not a scroll event

onEndReached is triggered by _maybeCallOnEndReached inside React Native whenever the bottom of the visible window falls within onEndReachedThreshold. The trap is that the FlatList considers itself "near the end" in three very common situations.

First, on the initial render when the data fits within the viewport. The last cell is already on screen, so onEndReached fires immediately. Second, right after a re-render where the list height changes. Even if the user has not moved, the recalculation can fire again. Third, when the screen is restored from a tab switch or a back navigation. If the scroll position was already near the bottom, the event fires once more.

In other words, onEndReached is not "the user scrolled to the bottom." It is "the last cell is now in the viewport." Writing a direct loadNextPage() call inside it without any guard is what causes the runaway requests.

The lightest fix: an onMomentumScrollBegin gate

The simplest and most reliable defense is to ignore onEndReached until the user has actually started a momentum scroll. onMomentumScrollBegin only fires for real, finger-driven scrolling, so it is the perfect signal that the user is genuinely paging through the list.

import { FlatList } from 'react-native';
import { useCallback, useRef } from 'react';
 
export function PostListScreen({ posts, loadNext }: Props) {
  const onEndReachedCalledDuringMomentum = useRef(true);
 
  const handleEndReached = useCallback(() => {
    if (onEndReachedCalledDuringMomentum.current) return;
    onEndReachedCalledDuringMomentum.current = true;
    loadNext();
  }, [loadNext]);
 
  return (
    <FlatList
      data={posts}
      keyExtractor={(item) => item.id}
      renderItem={({ item }) => <PostCard post={item} />}
      onEndReached={handleEndReached}
      onEndReachedThreshold={0.5}
      onMomentumScrollBegin={() => {
        onEndReachedCalledDuringMomentum.current = false;
      }}
    />
  );
}

The key detail is initializing the ref to true. That way the very first auto-fire on mount is ignored, and the flag only clears when the user has put a finger to the screen.

Add a duplicate-cursor guard inside the fetcher

A UI-side gate alone misses the case where momentum scrolling re-triggers the event right after the flag clears. Adding a guard inside the fetch function — refusing the same cursor twice — gives you a safety net that does not depend on the UI behaving perfectly.

import { useRef, useCallback, useState } from 'react';
 
export function usePaginatedPosts() {
  const [posts, setPosts] = useState<Post[]>([]);
  const [cursor, setCursor] = useState<string | null>(null);
  const inflightCursor = useRef<string | null | undefined>(undefined);
 
  const loadNext = useCallback(async () => {
    // Bail out if a request for the same cursor is already in flight
    if (inflightCursor.current === cursor) return;
    inflightCursor.current = cursor;
 
    try {
      const res = await fetch(`/api/posts?cursor=${cursor ?? ''}`);
      const data = await res.json();
      setPosts((prev) => [...prev, ...data.items]);
      setCursor(data.nextCursor);
    } finally {
      if (inflightCursor.current === cursor) {
        inflightCursor.current = undefined;
      }
    }
  }, [cursor]);
 
  return { posts, loadNext };
}

For a page-number API, swap the cursor check for "next page number is already in flight" — the principle is the same. Either way, the fetcher refuses to send a redundant request even if the UI hands it one.

Notes when you are using TanStack Query or SWR

If you are using TanStack Query's useInfiniteQuery, fetchNextPage already deduplicates inflight calls internally. When duplicate requests still slip through, the cause is almost always forgetting to check hasNextPage.

const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
  queryKey: ['posts'],
  queryFn: ({ pageParam }) => fetchPosts(pageParam),
  initialPageParam: null as string | null,
  getNextPageParam: (last) => last.nextCursor ?? undefined,
});
 
const handleEndReached = useCallback(() => {
  if (!hasNextPage || isFetchingNextPage) return;
  fetchNextPage();
}, [hasNextPage, isFetchingNextPage, fetchNextPage]);

Both hasNextPage and isFetchingNextPage matter. Without the second guard, a slow network still lets a second request leak out.

Gotchas when migrating to FlashList

Moving to Shopify's FlashList for performance changes the timing of onEndReached a little. FlashList uses recycling and an estimated item size to prefetch, so leaving onEndReachedThreshold at the default 0.5 fires it earlier than FlatList. In my apps, dropping it to 0.3 and tuning estimatedItemSize to a value close to the real measurement was enough to remove the surprise fires.

Diagnosing the issue from logs alone

To verify a fix, drop a timestamped log line into onEndReached:

const handleEndReached = () => {
  console.log('[onEndReached]', { cursor, now: Date.now() });
  // ...
};

If two calls land within 100 ms of each other, the cause is almost certainly a render-driven re-fire. If they are 500 ms apart, the user is probably pausing and resuming a scroll, or your flag is being cleared too early.

A four-step pre-ship checklist

Before pushing the change, walk through these four checks on a real device. They take less than two minutes and they catch almost every infinite-scroll regression I have seen.

  1. Open the list while the initial data is shorter than the viewport, and confirm loadNext did not fire on mount.
  2. Pull to refresh, and confirm loadNext did not fire as a side effect.
  3. Switch tabs and come back, and confirm loadNext did not fire on focus restore.
  4. Fling the list so it sticks to the bottom, and confirm only one request goes out for any given cursor.

The third one matters for monetized lists in particular — it is the easiest way to keep AdMob impression counts honest.

Infinite scroll is one of those features that looks like it works while quietly hammering your backend underneath. Once you understand that onEndReached is render-driven and add defenses on both the UI side and the fetcher side, you stop being the developer who finds out about a runaway loop from a billing alert at 2 a.m. Thanks for reading.

Share

Thank You for Reading

Rork Lab is ad-free, supported entirely by members like you. We publish practical guides daily with implementation code, benchmarks, and production-ready patterns. If you've found it useful, we'd love to have you on board.

  • Copy-paste ready implementation code
  • New advanced guides published daily
  • $5/mo or $10 for lifetime access
View Membership →

If you found this article helpful, a small tip ($1.50) would mean a lot to us. Your support helps keep this site ad-free and covers server and hosting costs.

Related Articles

Dev Tools2026-06-19
When Rork-Built Lists Stutter: Designing Image Caching and Prefetch
A FlatList from Rork starts stuttering once the images pile up. Here is how I restore smoothness with expo-image caching, recyclingKey, prefetch, and a move to FlashList, with the device numbers I measured.
Dev Tools2026-05-06
Animation Jank in Rork Apps — How I Diagnosed Frame Drops and Fixed Them
Animations in your Rork app running smoothly in the preview but stuttering on a real device? This guide explains how to diagnose frame drops, why they happen, and how one line of code often fixes the problem completely.
Dev Tools2026-05-02
When Your FlatList Starts Stuttering: Migrating Rork Apps to FlashList v2
When your Rork app's long lists start feeling sluggish, migrating to FlashList v2 makes scrolling dramatically smoother. Here is the practical migration path, taking advantage of v2's removal of estimatedItemSize.
📚RECOMMENDED BOOKS
Build a Large Language Model (From Scratch)
Sebastian Raschka
LLM Dev
Prompt Engineering for LLMs
Berryman & Ziegler
Prompting
AI Engineering
Chip Huyen
AI Eng
* Contains affiliate links
See all →