Pull to refresh is one component with ten props, and seven of them only work on one platform.
The component is RefreshControl. You put it inside a scrollable that scrolls vertically, you give it a boolean and a function, and each platform draws its own spinner from there. Its reference page lists ten props. Seven carry a platform badge, four for Android and three for iOS, so most of what you can change about how it looks you can only change on one side of the app. Everything here comes from the React Native documentation at version 0.87, read on 22 October 2026, and each figure is printed next to the last updated date of the page it came from. If you have not got as far as a scrolling screen yet, the place to start is make an app.
The rest of this page is the part that costs people an afternoon. Which scrollables accept the gesture, why the spinner sometimes stops before any data has arrived, and the one prop combination that makes a list ignore your refresh handler without complaining about it.
Start hereThe short version
Two props do the work, and the platform owns the rest.
Pull to refresh is one of the few mobile gestures where the platform does almost all of it for you. You are not writing a gesture handler, measuring a drag distance or animating a spinner. You are telling a scrollable that a refresh is possible, and telling it whether a refresh is currently happening. The threshold, the animation and the look of the indicator come from the native control on each platform, which is why the same code feels right on an iPhone and on a Pixel without a line of branching.
That also sets the limits. Because the control is native, you can only style it where a prop exists, and the props that exist are split by platform. A label under the spinner is an iOS feature. A second indicator colour, a larger indicator and a switch that turns the gesture off are Android features. Nobody gets both sets, and the documentation does not pretend otherwise.
Which component does React Native give you for this?
RefreshControl, and only that one. It sits in the core components list next to Pressable and ScrollView, and its reference page opens by saying the component is used inside a ScrollView to add pull to refresh functionality, and that when the scroll position is at zero, swiping down fires an onRefresh event. That second half matters more than it looks. The gesture only exists at the top of the content, so somebody halfway down a long list cannot set it off by accident while scrolling, and you do not have to write a rule to stop them.
Two props carry the whole interaction. refreshing is a boolean and it is the only prop on the page the reference marks as required: it says whether the view should currently be indicating an active refresh. onRefresh is a function and it runs when the view starts refreshing. The component also inherits View props, which is how you get style, testID and the accessibility props on it without them being listed again.
One wrinkle worth knowing before you go looking for it. The opening sentence of that reference page names ScrollView or ListView as the two places this component goes, and ListView is not in the current list of components. The Core Components and APIs page, which carries its own last updated date of 10 September 2026, gives RefreshControl a one line summary that names only ScrollView, and the word ListView appears nowhere on it. Read the older sentence as a ScrollView or anything built on top of one, which in practice means every list React Native ships.
Pick yours
What to pass, by scrollable
The answer changes depending on what the screen is scrolling. Pick the one you have. Every line below is taken from that component's own reference page in the React Native documentation, read on 22 October 2026.
FlatList
What you pass: onRefresh and refreshing. The list builds the control for you.
You almost never write RefreshControl by hand in a list. Pass a function and a boolean and the reference says a standard RefreshControl is added. FlatList inherits VirtualizedList props, so everything true of the base list is true here, progressViewOffset included.
Which of its props work on iOS, and which on Android?
Three props work on both platforms, three are badged iOS and four are badged Android. The badges are printed as text on the reference page rather than as icons, so the split is unambiguous. The table below is that page, reorganised so you can see the asymmetry in one look rather than by scrolling past ten headings.
| Prop | iOS | Android | Type and default | What it does |
|---|---|---|---|---|
| refreshing | Yes | Yes | boolean, required, no default | True while a refresh is in flight. The only prop the reference marks as required. |
| onRefresh | Yes | Yes | function | Runs when the view starts refreshing, which is the moment the gesture completes. |
| progressViewOffset | Yes | Yes | number, default 0 | Moves the indicator down from the top of the scrollable, in points. |
| tintColor | Yes | No | color | The colour of the spinner itself. |
| title | Yes | No | string | A line of text under the spinner. There is no Android equivalent on this page. |
| titleColor | Yes | No | color | The colour of that line of text. |
| colors | No | Yes | array of colors | One or more colours the indicator is drawn in. At least one is required if you pass it. |
| enabled | No | Yes | boolean, default true | Switches the gesture off without taking the control out of the tree. |
| progressBackgroundColor | No | Yes | color | The colour of the disc behind the spinner. |
| size | No | Yes | enum of default or large, default is default | Two sizes of indicator, and nothing between them. |
Three consequences come out of that table, and each one has caught somebody. The first is the label. A line of text under the spinner, the thing people use for a last updated time, exists only on iOS through title and titleColor. There is no Android counterpart listed, so if you want that line on both platforms it belongs in your own layout above the first row, where you control it anyway.
The second is turning the gesture off. enabled does precisely that and defaults to true, and it is badged Android only. On iOS there is no documented prop for it, so you either stop passing a refresh handler or you leave the control out of the render for that state. It is a conditional instead of a prop, and it behaves the same on both platforms, which is usually the version you want in the code anyway.
The third is colour. iOS gives you one tintColor for the spinner. Android gives you colors, which is an array needing at least one entry, plus progressBackgroundColor for the disc behind it and size for a choice between a default and a large indicator. If your design has one brand colour, pass it as tintColor on iOS and as a one entry array on Android and the two will match. If the design has a gradient or a sequence, only Android can express it.
React Native, RefreshControl props, ten props with seven platform badges, read 22 October 2026
Does it work inside every scrollable, or only some?
Only vertical ones, and the documentation says so twice in two different places. The ScrollView reference has a single prop for this, refreshControl, which takes a RefreshControl element and whose description ends by saying it only works for vertical ScrollViews and that the horizontal prop must be false. ScrollView lists no onRefresh and no refreshing of its own, so on a plain ScrollView you build the element yourself and hand it to that prop. That page carries its own last updated date of 14 October 2026.
Lists work differently and more conveniently. VirtualizedList, which is the engine under the lists you actually use, documents onRefresh and refreshing directly: provide a handler and the reference says a standard RefreshControl is added for pull to refresh, with a reminder to set the refreshing prop correctly. FlatList inherits VirtualizedList props, in those words on its own page, and SectionList is documented as a convenience wrapper around VirtualizedList that also inherits ScrollView props. So the same two props cover all three, and both list pages carried a last updated date of 19 October 2026 when they were read.
That leaves the cases where it does not work at all. Horizontal is the big one: ScrollView describes horizontal as arranging children in a row instead of a column, it defaults to false, and setting it true puts you outside what refreshControl supports. VirtualizedList repeats the restriction for itself. A sideways carousel has no top edge to pull away from, so a refresh there needs a button or a timer instead. If your list is long enough that refreshing the top is only half the problem, the other half is in add infinite scroll.
React Native, VirtualizedList reference, last updated 12 August 2026, read 22 October 2026
Why does the spinner stop before the data arrives?
Because refreshing is a controlled prop, and the reference page says it in a note rather than in the prop table, which is why it gets missed. The indicator is visible while that boolean is true and gone when it is false. If onRefresh fires a request and nothing sets the flag, the indicator stops as soon as your finger leaves the screen. The note spells out the fix: set it to true inside the onRefresh function. Set it false when the work finishes, and set it false on the failure path too, because a dropped request with no error handler leaves a spinner turning until the user force quits.
The second failure is quieter and harder to spot. If you pass refreshControl to a list and also pass onRefresh and refreshing, the list uses your element and ignores both props. The VirtualizedList reference states it plainly: a custom refresh control overrides the control built internally, and onRefresh and refreshing are also ignored. Nothing warns you. The gesture works, the spinner appears, and your handler never runs. Pick one of the two approaches per list and delete the other, rather than keeping both and hoping.
The third is cosmetic until somebody screenshots it. A translucent or sticky header will sit on top of the indicator, and the prop for that is progressViewOffset, a number with a default of 0 and no platform badge. The SectionList page gives the concrete version, offsetting the control from the top by 100 points, and the FlatList page describes the same prop as the one to set when an offset is needed for the loading indicator to show correctly. Reach for it before you reach for padding on the list, because padding moves the content as well.
One last thing that is not in the documentation because it is not a prop. Decide what a refresh means in your app before you wire it up. Refetching the first page of a list is the common case. Refetching the first page while the user has unsaved input on the screen is a data loss bug wearing a gesture, and it shows up in forms more than lists, which is the same class of problem described in add form validation.
What this looks like in an app you did not hand write
It looks the same, if the thing you generated is a real project. Newly writes a React Native and Expo codebase that you own, with two-way GitHub sync, so the list on your screen is a React Native list with props you can read and change. Pull to refresh is two props and a piece of state in that file, which means you can ask for it in plain English or add it yourself afterwards without either route being a dead end. It is $25 a month and there is no free tier.
This is the practical reason to care whether a builder hands you source. A gesture like this one is a small change to a list component, and in a project you own it stays a small change. In a product that renders your screens from a configuration you cannot reach, it is a feature request. The difference only shows up on the second or third thing you want to adjust, which is late enough to be expensive.
Newly uploads iOS builds to TestFlight, publishes to Google Play internal testing and builds a standalone Android APK, so the refresh you just added is testable on a real device the same day. It does not press submit on the App Store for you, because Apple requires the owner of the content to submit, and that step with its own requirements is covered in publish to app store.
Questions people ask about pull to refresh in React Native
RefreshControl. It is a core component with its own reference page, and the documentation describes it as the thing you put inside a ScrollView to add pull to refresh, with the gesture firing when the scroll position is already at zero. You pass it a boolean called refreshing and a function called onRefresh. In a FlatList or a SectionList you normally never write the element at all: you pass those same two props to the list, and the reference says a standard RefreshControl is added for you. Read on 22 October 2026 against documentation version 0.87.
Two props, one boolean, and a decision about what a refresh means.
Write down which screen needs the gesture and what it should refetch when the user pulls. That sentence is the whole specification, and the code underneath it is smaller than the sentence.
Start building