Articles · How toUpdated October 2026

Infinite scroll is two props and one flag, and the flag is the part nobody writes.

In a React Native app you add infinite scroll with onEndReached, which runs when the reader scrolls near the bottom, and onEndReachedThreshold, which decides how near. Both are documented, both take one line, and neither of them stops your loader being called a second time while the first request is still open. That second call is where the duplicated rows and the doubled API bill come from, and the documentation is clear enough about it if you read the exact words. If you have not built the list itself yet, start with make an app.

Everything below about the props comes from React Native documentation, read on 22 October 2026, and each page carries its own last-updated date, printed next to the figures taken from it. The one thing worth reading twice is the second section.

Start here

The short version

The list tells you when. You decide whether.

Set onEndReached to a function that fetches your next page and appends it. Set onEndReachedThreshold if the default of two screens of warning is too much for you. Put a spinner in ListFooterComponent. That is the whole documented surface, and it is genuinely small.

Then add two booleans of your own. One says a request is already running, and the callback returns early when it is true. One says the server has no more rows, and the callback returns early when that is true as well. Four lines, written by you, because React Native does not write them and does not claim to. Skip them and the page works on your desk, where a local API answers in nine milliseconds, and falls apart on a train.

What does React Native give you for loading more rows?

Two props, and they are documented in a place most people do not look. The FlatList page does not list either of them. It describes FlatList as a convenience wrapper around VirtualizedList that inherits its props, as well as those of ScrollView, where they are not listed explicitly. onEndReached and onEndReachedThreshold are two of the props that live on the VirtualizedList page. If you have ever scanned the FlatList props, failed to find onEndReached and concluded you were remembering a different library, that is why.

The callback is documented as called once when the scroll position gets within onEndReachedThreshold from the logical end of the list. Its type is given as a function taking an object with a distanceFromEnd number, so you are handed the remaining distance rather than an index or a row. That number is useful for logging and almost never useful for logic.

The threshold is the prop people set wrong, because the unit is surprising. It is documented as how far from the end, in units of visible length of the list, the trailing edge of the list must be from the end of the content to trigger the callback, with a worked example: a value of 0.5 triggers onEndReached when the end of the content is within half the visible length of the list. The default is 2. Two visible lengths, which on a phone means two whole screens of content still sitting below the fold before anything is requested. Read on 22 October 2026 from a page whose own footer says it was last updated on 12 August 2026.

That unit is worth holding on to, because a second prop on the same page uses it. windowSize is documented as the maximum number of items rendered outside the visible area, in units of visible lengths, with a default of 21, which the documentation spells out for a list that fills the screen as the visible screen area plus up to ten screens above and ten below the viewport. So the list is already keeping twenty screens of rows alive while asking you for more two screens early. Both numbers are generous, and on a list of image cards both are worth lowering.

Pick a number

Where onEndReachedThreshold actually fires

The unit is not pixels and it is not rows. React Native documents it as units of the visible length of the list, so the number you type is multiplied by the height of the window the reader is looking at. The row counts below assume a list that shows eight rows at a time, which is about right for a 90 point row on a phone.

Fires when: Two full screens from the bottom, about sixteen rows on a list showing eight.

What that costs: This is what you get if you never set the prop. It is also why so many people meet the double call: if the page you just appended is shorter than two screens, the documented trigger condition is still true the moment the new rows render.

The unit, the default of 2 and the worked example at 0.5 are from React Native documentation. The row counts and the judgements are mine.

React Native, VirtualizedList, last updated on 12 August 2026, read 22 October 2026

Why does the callback fire again while the first load is running?

This is the question the whole page exists for, so here is the careful version. The documentation says the callback is called once when the scroll position gets within onEndReachedThreshold from the logical end of the list. Read that sentence for what it scopes. The once attaches to the scroll position getting within the threshold, so it means one call for each arrival. The sentence says nothing about how many arrivals you get over the life of the list, or how many you get per page of data.

Now put the default threshold back in. The trigger condition is that the end of your content sits within two visible lengths of the trailing edge. Suppose your API returns ten rows and ten rows comes to a screen and a half. You append them, the list grows, and the end of the content is now a screen and a half below the fold, which is still inside two. The documented condition that fired the callback is still true. The reader keeps their thumb moving, another scroll event lands, and you are in the same state that produced the first call.

So what does React Native promise about that? Nothing, and I went looking properly rather than assuming. On 22 October 2026 I pulled every page of the current documentation on reactnative.dev, 225 of them, and searched the text. onEndReached appears on one page, VirtualizedList, and the only thing said about repeat calls is the word once in that single sentence. There is no documented in-flight check, no debounce, no queue, and no note telling you what happens if the condition is still true after your data arrives. The silence is the answer: the guard is yours to write.

Write it as a flag rather than a clever comparison of distanceFromEnd. Set the flag true before the request goes out, set it false in a finally block so a failed request does not leave the list permanently silent, and return early from the callback while it is true. Add a second flag for the end of the data, because a server that answers with an empty array will otherwise be asked again every time the reader twitches. Both flags belong in the same state the footer spinner reads from.

One more trap in the same area, and it bites hardest on a tablet. If your first page is shorter than the screen, the end of the content is within the threshold from the moment the list mounts, so the callback can run before the reader has scrolled at all. A list that asks for page two and page three before anybody touches it is the same bug wearing a different hat, and the same flag fixes it. The way filters interact with this is its own job, which is why resetting the page number belongs with add filters and sorting.

React Native, VirtualizedList, onEndReached and onEndReachedThreshold, read 22 October 2026

Which half of this is documented

Infinite scroll feels like a feature you turn on. It is closer to a handshake, and the two sides are not the same size. Here is the split, with the documented column taken from the React Native props and the other column taken from every list I have had to fix.

The jobIn React Native documentationWhat you still write
Knowing when the reader is near the endYes, onEndReachedNo
Choosing how early that happensYes, onEndReachedThreshold, default 2Pick the number
Knowing how far from the end you areYes, distanceFromEnd on the callback argumentNo
Not calling your loader twiceNoYour own in-flight flag
Knowing there are no more pagesNoYour own end-of-data flag
Somewhere to put the spinnerYes, ListFooterComponentThe spinner itself
Stable keys for appended rowsYes, keyExtractorAn id from your API that does not move
Re-rendering after you appendYes, if you hand it a new arrayNever mutate the old one
Retrying a page that failedNoAll of it

Why do the new rows duplicate, or fail to appear at all?

Two different faults look identical from the sofa, and the FlatList page documents both causes. The first is keys. React Native describes keyExtractor as extracting a unique key for an item at a given index, used for caching and as the React key to track item re-ordering, and it tells you what happens when you leave it out: the default extractor checks item.key, then item.id, and then falls back to using the index, like React does. That fallback is quiet and it is fine until the list grows. Once page two arrives, the twentieth row of the old data and the twentieth row of the new data have both been keyed by position at some point, and React has no way to tell you which card it is meant to keep. Pass keyExtractor an identifier your API guarantees, and if it does not guarantee one, that is a conversation with whoever owns the endpoint rather than something to paper over on the client.

The second fault is the opposite symptom: you append rows and the screen does not change. FlatList is documented as a PureComponent, which the page spells out as meaning it will not re-render if its props remain shallow-equal, and it tells you to make sure everything your renderItem function depends on is passed as a prop that is not identical after updates, including the data prop and parent component state. Pushing onto the array you already handed over leaves the prop pointing at the same array, so it is shallow-equal to itself and nothing redraws. Hand over a new array every time. If something outside the data changes too, the documented place for it is extraData, described as a marker property for telling the list to re-render, to be treated immutably.

One caveat on the same page catches people out on long lists. Internal state is not preserved when content scrolls out of the render window, and the page says to make sure all your data is captured in the item data or an external store. A row with a text input or a toggle inside it will forget what the reader did once it leaves the window and comes back. That matters most when rows are editable, which is the point where add form validation stops being a separate problem.

React Native, FlatList, keyExtractor and the PureComponent caveats, last updated on 19 October 2026, read 22 October 2026

What should you set before you blame the list?

Start with initialNumToRender, which is documented with a default of 10 and the advice that it should be enough to fill the screen but not much more. The page adds a detail worth knowing: those initial items are never unmounted by the windowed rendering, which the documentation says is there to improve the perceived performance of scroll-to-top actions. Ten is low for a tablet and high for a row with three images in it, and it is the first number to try when the first screen looks empty for a beat.

If your rows are a fixed height, give the list getItemLayout. React Native documents it as an optional optimisation that lets the list skip measuring dynamic content when you already know the size, and says it can be a great performance boost for lists of several hundred items, which is exactly the size an infinite list reaches after a few pages. The page also warns you to include the separator height in the offset calculation if you are using ItemSeparatorComponent, which is the kind of detail that costs an afternoon when you miss it.

Then accept one thing the documentation is honest about. To constrain memory and keep scrolling smooth, content is rendered asynchronously offscreen, so it is possible to scroll faster than the fill rate and momentarily see blank content. The page calls it a tradeoff. A reader who flicks hard will see white, and no threshold value prevents that, because the gap is in rendering rather than in fetching. Give the blank space a placeholder row of the right height instead of trying to outrun it.

If you would rather describe the behaviour than wire it up, that is what Newly is for. You say what the screen shows and how it pages, and it writes a real React Native and Expo project you own, with two-way GitHub sync so the list code is somewhere you can read and change. It uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK. It costs $25 a month and there is no free tier. It will not press submit on the App Store for you, because Apple requires the owner of the content to submit, so that last step is yours and the mechanics are in publish to app store. Whichever way you build it, test the list once with the network throttled to something slow. Every bug on this page is invisible at full speed.

React Native, FlatList, initialNumToRender and getItemLayout, read 22 October 2026

Questions people ask about infinite scroll in a mobile app

Two props on a FlatList, plus one boolean you write yourself. onEndReached is the callback React Native runs when the reader gets near the bottom, and onEndReachedThreshold decides how near. The boolean is a loading flag that you check at the top of the callback and return early on, because nothing in React Native keeps the callback from running while your request is still open. Then you need a second flag for the end of the data, so the list stops asking once the server has run out of rows. The two props are documented, the two flags are yours.

Two props, two flags, one slow connection to test on.

Write down how many rows a page returns, what the unique id on each row is called, and what the server sends when it has run out. Those three answers are the whole brief for a list that pages properly.

Start building