How do you add pull-to-refresh to a React Native FlatList, and what do its refreshing and onRefresh props do?
answer
- passing a callback adds the control
- refreshing is a controlled boolean
- set it true inside the handler
- replace page one, do not append
- custom refreshControl overrides both props
basics
~20 sPassing onRefresh makes a React Native FlatList add a standard RefreshControl; refreshing is the controlled boolean that shows the indicator. Set refreshing true when the reload starts and false when it ends, and replace the first page rather than appending.
solid answer
~40 s`FlatList` gets pull-to-refresh from two props. `onRefresh` is the callback: when it is present, the list renders its scroll view with a built-in `RefreshControl`, and the pull gesture calls it. `refreshing` is a **controlled** boolean: the indicator shows while it is `true`, and if the handler never sets it to `true` the indicator stops immediately. React Native requires `refreshing` to be a boolean whenever `onRefresh` is set; leaving it undefined throws. The handler should set `refreshing` true, fetch page 1, **replace** the data, reset the paging position and `hasMore`, and set `refreshing` false in a `finally`. The list does not await a promise returned from `onRefresh`, so the flag, not the promise, controls the spinner. For pagination, show a footer spinner instead; `refreshing` belongs to the pull only.
code
tsx · 31 linesimport { useCallback, useState } from 'react';
import { FlatList, Text } from 'react-native';
type Story = { id: string; title: string };
declare function fetchStories(page: number): Promise<Story[]>;
export function NewsFeed({ initial }: { initial: Story[] }) {
const [stories, setStories] = useState(initial);
const [refreshing, setRefreshing] = useState(false);
const onRefresh = useCallback(async () => {
setRefreshing(true); // controlled: without this the indicator stops at once
try {
setStories(await fetchStories(1)); // replace, never append
} catch {
// keep the stories already on screen; surface the error elsewhere
} finally {
setRefreshing(false);
}
}, []);
return (
<FlatList
data={stories}
keyExtractor={story => story.id}
renderItem={({ item }) => <Text>{item.title}</Text>}
refreshing={refreshing}
onRefresh={onRefresh}
/>
);
}go deeper
Recall the pair: onRefresh turns pull-to-refresh on, refreshing shows the indicator. Set refreshing true when the reload starts and false when it ends, including on errors.
Explain why refreshing is controlled, why an undefined value throws when onRefresh is set, and why an async handler's promise does not keep the indicator alive.
Keep the pull and the next-page load as separate states, reset the paging position on refresh, and make sure a page request pending during a refresh cannot be appended afterwards.
Decide what a refresh means for the product - reload page one, fetch only newer stories, or keep the reader's position - and make the list's loading states agree with that choice.
## Two props, one built-in control Pull-to-refresh on a React Native `FlatList` (and on `SectionList` and `VirtualizedList`, which share the props) comes from a pair of props: | Prop | Type | What it does | |---|---|---| | `onRefresh` | `() => void` | Its presence makes the list render its scroll view with a standard `RefreshControl`; a pull at the top of the list calls it. | | `refreshing` | `boolean` | The controlled state of that indicator: `true` shows it, `false` hides it. | | `progressViewOffset` | `number` | Shifts the indicator down, for example below a translucent header drawn over the list. | | `refreshControl` | element | A custom control element; when set, it replaces the built-in one and the list ignores `onRefresh` and `refreshing`. Vertical lists only. | You do not render a `RefreshControl` yourself for the common case - supplying `onRefresh` is enough. ## refreshing is controlled **Controlled** means the indicator mirrors your prop, not the gesture. Three consequences follow, each checked in the React Native 0.87 source: - **`refreshing` must be a boolean.** If `onRefresh` is set and `refreshing` is `undefined`, `VirtualizedList` throws an invariant error: "`refreshing` prop must be set as a boolean in order to use `onRefresh`". - **Set it `true` inside the handler.** The pull starts the native indicator, but the component then syncs it back to your prop. The `RefreshControl` source says so directly: `refreshing` needs to be set to `true` in `onRefresh`, otherwise the indicator stops immediately. - **The returned promise is ignored.** `onRefresh` may be an `async` function, but the control calls it without awaiting the result. A spinner that should last as long as the fetch must be driven by state you reset when the fetch settles. ## What a refresh does to the feed In a news feed that loads 20 stories per page, a refresh means "start over from the newest page": 1. Set `refreshing` to `true`. 2. Fetch page 1. 3. **Replace** the list's data with it - appending would duplicate stories already on screen. 4. Reset the next-page position to 2 and recompute `hasMore` from the page size. 5. Set `refreshing` to `false` in a `finally` block, so a network error cannot leave the indicator spinning. A refresh that fails should normally keep the stories already shown and report the error, rather than clearing the list. ## Keep refresh and pagination apart Two loading states live on a feed, and each has its own indicator: - **Refresh** - the pull indicator at the top, driven by `refreshing`. - **Next page** - a spinner in `ListFooterComponent`, driven by your own `loadingMore` state. Setting `refreshing` to `true` for a next-page load is a classic mistake: the indicator appears at the top of the list while the reader is at the bottom, and on a pull-enabled list it signals "the whole feed is reloading". The two also interact: a page request that is still pending when a refresh starts must not be appended afterwards, so the page loader should skip while a refresh runs. ## Customising the indicator The built-in control takes its defaults. When you need a tint colour or a title, pass your own `RefreshControl` element through `refreshControl`. At that point the element's own `refreshing` and `onRefresh` props are the ones that count, and the list's props of the same names are ignored. ## First load versus refresh The first load of the screen and a pull-to-refresh fetch the same page, but they are different states. The first load has nothing to show yet, so it usually renders a full-screen placeholder or a `ListEmptyComponent`; the pull indicator is for a list the reader can already see. Driving the first load through `refreshing` couples the two, and an error then leaves the reader with an empty list and an indicator that has just vanished. Keep a separate `initialLoading` state, and reserve `refreshing` for a reload the reader asked for. ## Common mistakes - Passing `onRefresh` without `refreshing`, which throws. - Leaving `refreshing` false during the fetch, so the indicator snaps back at once. - Forgetting to reset `refreshing` on an error path, so the indicator never stops. - Appending the refreshed page to the existing data. - Expecting the indicator to wait for an `async` handler's promise.
- Why does the pull indicator stay visible for the whole fetch in the example, given that FlatList ignores the promise onRefresh returns?Because the handler sets `refreshing` to `true` before awaiting and back to `false` in `finally`. The control is controlled: it mirrors that prop, so the indicator lasts exactly as long as the state says. The promise returned by the async handler is never awaited by the list; if the handler skipped the state update, the indicator would stop right after the pull.
- When would you pass refreshControl instead of onRefresh and refreshing?When the built-in control's defaults are not enough - a brand tint colour or a title. You pass your own `RefreshControl` element through `refreshControl`; the list then ignores its own `onRefresh` and `refreshing`, and the element's props of the same names drive the indicator. It works only on vertical lists.
saying these in an interview costs you the question
- FlatList keeps the refresh indicator visible until onRefresh's promise resolves.
- The refreshing prop is optional when onRefresh is set; it defaults to false.
- A refresh should append the newly fetched first page to the existing stories.
- Set refreshing to true while loading the next page at the bottom of the feed.
- Pull-to-refresh on a FlatList requires rendering a RefreshControl child yourself.