Given the rise in user expectations for flawless digital experiences, it’s important to ensure your mobile applications function intuitively and correctly. For example, when mobile users reach the bottom of a scrolling list and continue pulling down, they anticipate that the app will automatically fetch the next set of results.
In this article, we’ll investigate how to implement pull-to-refresh and scroll-to-refresh functionality in a React Native application using a FlatList component. As a bonus, we’ll examine how to customize a RefreshControl component by changing different parameters, like size and color.
In the tutorial portion of this article, we’ll build a React Native application that refreshes like this:

Jump ahead:
The Replay is a weekly newsletter for dev and engineering leaders.
Delivered once a week, it's your curated guide to the most important conversations around frontend dev, emerging AI tools, and the state of modern software.
To follow along with this tutorial, you can refer to this GitHub repo; it contains all of the code used in this article. However, I also recommend you generate an entirely new Expo React Native application by following this guide.
Once the project is generated, we’ll need to create some folders. Here’s how the folder structure will look:
// Folder structure app_name ┣ assets ┃ ┣ adaptive-icon.png ┃ ┣ favicon.png ┃ ┣ icon.png ┃ ┗ splash.png ┣ components ┃ ┣ Item.jsx ┃ ┗ Loader.jsx ┣ hooks ┃ ┗ useFetchUser.js ┣ screens ┃ ┣ BottomLoader.jsx ┃ ┣ Combined.jsx ┃ ┗ TopLoader.jsx ┣ .gitignore ┣ App.js ┣ app.json ┣ babel.config.js ┣ package-lock.json ┗ package.json
A React Hook is a special kind of function that allows us to manage states without writing a class or a component. We’ll set up a custom Hook to make API calls to a server to fetch data and then store it in a state to be used in our components.
To start, install Axios by running the following command in the terminal:
npm i axios
Then open the hooks/useFetchUser.js file, import necessary dependencies, and declare some constants:
// JavaScript
// useFetchUser.js
import React, { useState } from 'react';
import axios from 'axios';
export const FETCH_RESULTS = 10;
export const MAX_LENGTH = 50;
Next, let’s create the useFetchUser Hook and declare some states:
// JavaScript
// useFetchUser.js
export const useFetchUser = () => {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [success, setSuccess] = useState(false);
// Code comes below here
// Code comes above here
return { users, isLoading, success, getUsers, errorMessage };
};
Now, we’ll create a function that will enable us to make API calls and update the states based on the responses. For this example, we’ll make use of the pagination API from https://randomuser.me/api:
// JavaScript
// useFetchUser.js
const getUsers = async (currentPage, isTop = false) => {
setIsLoading(true);
setSuccess(false);
setErrorMessage('');
try {
const { data } = await axios.get(
`https://randomuser.me/api/?page=${currentPage}&results=${FETCH_RESULTS}`
);
if (isTop) {
const newList = [...data?.results, ...users];
const slicedUsers = [...newList].slice(0, MAX_LENGTH);
setUsers(slicedUsers);
}
if (!isTop) {
const randomIndex = () => Math.ceil(Math.random() * 10);
const newList = [...users, ...data?.results];
const slicedUsers = [...newList].slice(-1 * MAX_LENGTH + randomIndex());
setUsers(slicedUsers);
}
setSuccess(true);
} catch (error) {
const theError =
error?.response && error.response?.data?.message
? error?.response?.data?.message
: error?.message;
setErrorMessage(theError);
}
setIsLoading(false);
};
Combined, the code should look similar to this:
// JavaScript
// useFetchUser.js
import React, { useState } from 'react';
import axios from 'axios';
export const FETCH_RESULTS = 10;
export const MAX_LENGTH = 50;
export const useFetchUser = () => {
const [users, setUsers] = useState([]);
const [isLoading, setIsLoading] = useState(false);
const [errorMessage, setErrorMessage] = useState('');
const [success, setSuccess] = useState(false);
const getUsers = async (currentPage, isTop = false) => {
setIsLoading(true);
setSuccess(false);
setErrorMessage('');
try {
const { data } = await axios.get(
`https://randomuser.me/api/?page=${currentPage}&results=${FETCH_RESULTS}`
);
if (isTop) {
const newList = [...data?.results, ...users];
const slicedUsers = [...newList].slice(0, MAX_LENGTH);
setUsers(slicedUsers);
}
if (!isTop) {
const randomIndex = () => Math.ceil(Math.random() * 10);
const newList = [...users, ...data?.results];
const slicedUsers = [...newList].slice(-1 * MAX_LENGTH + randomIndex());
setUsers(slicedUsers);
}
setSuccess(true);
} catch (error) {
const theError =
error?.response && error.response?.data?.message
? error?.response?.data?.message
: error?.message;
setErrorMessage(theError);
}
setIsLoading(false);
};
return { users, isLoading, success, getUsers, errorMessage };
};
Here are some details to better understand what’s going on in the above code. The getUsers function is asynchronous and accepts currentPage and isTop as parameters. The currentPage parameter is passed to the URL as instructed in https://randomuser.me in order to fetch the paginated items. The isTop parameter is used to determine if the user is refreshing content from the top or from the bottom.
We set the loading, success, and error message states to true, false, and an empty string, respectively. Next, we create a try-catch block. In the try part, we make an API call with axios and pass FETCH_RESULTS to the URL. We then expect the length of the data returned from the response to equal the value of FETCH_RESULTS. In other words, if the value of FETCH_RESULTS is 10, the API will return 10 results.
In the first if statement, we check that isTop is true. Inside the code block, we create one list that prepends to the existing users array and a second list that slices our newList to ensure we are within the desired MAX_LENGTH of data before actually updating the user list.
In the second if statement, we create an inline function to generate a random number. Next, we append the newly received data to the existing user list and then slice the list we’re creating to get a relatively random number of list results all the time. This ensures that users do not experience any flickering or stuttering when scrolling from the bottom of the list.
In the catch block, we extract any error and then set it to the error message state; outside the try-catch block, we set loading state to false. Finally, we return isLoading, success, users, errorMessage, and getUsers so that they can be used whenever we call the useFetchUser Hook anywhere in the application.
The loader component will be used to indicate when the API fetching is in progress. To build the loader, we need to import necessary dependencies and then pass the isLoading and withText parameters. The most important dependency is the ActivityIndicator which handles the actual loading indicator.
Here’s the code for our loader component:
// JavaScript
// components/Loader.jsx
import { StyleSheet, View, ActivityIndicator, Text } from 'react-native';
export const Loader = ({ isLoading = false, withText = false }) => {
return isLoading ? (
<View style={styles.loader}>
<ActivityIndicator size='large' color='#aaaaaa' />
{withText ? (
<Text style={{ color: 'green' }}>Loading users...</Text>
) : null}
</View>
) : null;
};
const styles = StyleSheet.create({
loader: {
marginVertical: 15,
alignItems: 'center',
},
});
To style the loader, we simply added a vertical margin and centered the components.
The Item.jsx component is used to represent the rendered item of the FlatList components that we’ll build. The Item function accepts item and index parameters and returns a View with an Image and a second View component.
The second View component returns two Text components that we use to display the name and email from the item parameter. The Image component displays a large picture also from the item parameter.
Here’s the code for our Item component:
// JavaScript
// components/Item.jsx
import { StyleSheet, View, Text, Image } from 'react-native';
export const Item = ({ item, index }) => {
return (
<View style={styles.itemWrapper}>
<Image source={{ uri: item?.picture?.large }} style={styles.itemImage} />
<View style={styles.itemContentWrapper}>
<Text
style={styles.itemName}
>{`${item?.name?.title} ${item?.name?.first} ${item?.name?.last}`}</Text>
<Text style={styles.itemEmail}>{`${item?.email}`}</Text>
</View>
</View>
);
};
const styles = StyleSheet.create({
itemWrapper: {
flexDirection: 'row',
paddingHorizontal: 16,
paddingVertical: 16,
borderBottomWidth: 1,
borderColor: '#dddddd',
},
itemImage: {
width: 51,
height: 51,
marginRight: 15,
},
itemContentWrapper: {
justifyContent: 'space-around',
},
itemName: {
fontSize: 17,
},
itemEmail: {
color: '#777777',
},
});
We’ll create a TopLoader.jsx screen to demonstrate to the user how to handle pull-to-refresh at the top of the list. As a first step in building this screen, we import necessary dependencies, including the FETCH_RESULTS, useFetchUser.js, Item.jsx, and Loader.jsx files:
// JavaScript
// screens/TopLoader.jsx
import { StyleSheet, Text, View, FlatList, RefreshControl } from 'react-native';
import React, { memo, useCallback, useEffect, useRef, useState } from 'react';
import { FETCH_RESULTS, useFetchUser } from '../hooks/useFetchUser';
import { Item } from '../components/Item';
import { Loader } from '../components/Loader';
Next, we create the component and initialize the states:
// JavaScript
// screens/TopLoader.jsx
const TopLoader = () => {
const flatListRef = useRef(null);
const { isLoading, success, users, errorMessage, getUsers } = useFetchUser();
const [currentPage, setCurrentPage] = useState(1);
const [refreshing, setRefreshing] = useState(false);
return (
<View>
</View>
);
};
export default memo(TopLoader);
Here, we define a flatListRef that we use to control automatic scrolling. We also call the useFetchUser Hook and destructure the following data from it: isLoading, success, users, errorMessage, and getUsers.
Next, we define the state to handle the current page, which we pass to the getUsers function, and also the refreshing state that will control the visibility of the pull-to-refresh loader.
We create a function to increment the current page state and another function to update the refreshing state:
// JavaScript
// screens/TopLoader.jsx
const loadMoreItem = () => {
setCurrentPage((prev) => prev + 1);
};
const onRefresh = useCallback(() => {
setRefreshing(true);
loadMoreItem();
}, []);
In the below code, we create a scrollToItem function that serves as a crucial addition to our FlatList. This function automatically handles scrolling when our data changes:
// JavaScript
// screens/TopLoader.jsx
const scrollToItem = (index) => {
flatListRef.current.scrollToIndex({ index: index, animated: false });
};
The scrollToItem function is very important because when new data is added at the beginning of the FlatList, React Native will attempt to display the initial set of items. This behavior can be perplexing for users, who would naturally anticipate continuing their scrolling from the point at which they refreshed the content.
Now, we simply check for a successful API request and then set the refreshing state to true. We also call the scrollToItem function and pass the FETCH_RESULTS - 1 as the index parameter:
// JavaScript
// screens/TopLoader.jsx
useEffect(() => {
if (success) {
setRefreshing(false);
scrollToItem(FETCH_RESULTS - 1);
}
}, [success]);
We can also add code to handle the API call on initial page load and whenever the current page changes:
// JavaScript
// screens/TopLoader.jsx
useEffect(() => {
getUsers(currentPage, true);
}, [currentPage]);
As you may have observed while using the useEffect Hook, we included additional values in an array known as dependencies. These dependencies play a crucial role in how the useEffect Hook operates.
If we omit the dependency array, our code will execute with every re-render, and if we supply an empty array as the dependency the useEffect Hook will run only once. However, when we specify particular values within the dependency array, the Hook will trigger during the initial render and whenever the values of those dependencies change.
Now it’s time to add our jsx markup. Begin by adding the following skeleton:
// JavaScript
// screens/TopLoader.jsx
<View>
<Text
style={{
textAlign: 'center',
paddingVertical: 10,
fontSize: 18,
fontWeight: '600',
}}
>
Pull To Refresh Control
</Text>
<FlatList
ref={flatListRef}
data={users}
renderItem={Item}
keyExtractor={(item) => item?.email}
/>
{errorMessage ? <Text>{errorMessage}</Text> : null}
</View>
In this markup, we simply have a Text component, a FlatList, and a second Text component that is used to render any error message that may have been passed from the useFetchuser Hook.
Our FlatList component accepts a ref that we previously defined above. It also accepts data, in the form of users from useFetchUser Hook, a renderItem which is the Item component, and then the keyExtractor, which is the email of each user.
In order to measure our progress, we‘ll need to render our TopLoader on the screen. To do so, add the following code to the App.js file:
// JavaScript
// App.js
import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import TopLoader from './screens/TopLoader';
const App = () => {
return (
<View style={styles.container}>
<TopLoader />
</View>
);
};
export default App;
const styles = StyleSheet.create({
container: {
flex: 1,
marginVertical: 40,
marginHorizontal: 20,
},
});
Now we can open the terminal and run the npm run start command to start up the server and then press a or i to run the app on Android or iOS, respectively.
Next, let’s go back to the TopLoader.jsx component and add the refreshControl prop to the FlatList component:
// JavaScript
// screens/TopLoader.jsx
<FlatList
ref={flatListRef}
data={users}
renderItem={Item}
keyExtractor={(item) => item?.email}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
/>
}
/>
The RefreshControl component is provided by the React Native team to handle pull-to-refresh at the top of a FlatList, ScrollView, or ListView. This component accepts a couple of props, but only the refreshing props is required to make it work. Therefore, we need to pass the refreshing state we defined above to this component.
The next prop is onRefresh which is called when refreshing has begun. For this project, we’ll pass the onRefresh function we created above so that when we refresh, the onRefresh callback will trigger the getUsers function in the useEffect Hook to run.
If we test our application’s pull-to-refresh functionality at this point, we’ll see an error: “Layout doesn’t know the exact location of the requested element. Falling back to calculating the destination manually.” To address this issue, let’s add the onScrollToIndexFailed prop to our FlatList component, like so:
// JavaScript
// screens/TopLoader.jsx
<FlatList
ref={flatListRef}
data={users}
renderItem={Item}
keyExtractor={(item) => item?.email}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
/>
}
// Layout doesn't know the exact location of the requested element.
// Falling back to calculating the destination manually
onScrollToIndexFailed={({ index, averageItemLength }) => {
flatListRef.current?.scrollToOffset({
offset: index * averageItemLength,
animated: true,
});
}}
/>
Our pull-to-refresh functionality should be working seamlessly now, but we can further improve it by adding three additional props: initialScrollIndex, maxToRenderPerBatch, and ListEmptyComponent:
// JavaScript
// screens/TopLoader.jsx
<FlatList
ref={flatListRef}
data={users}
renderItem={Item}
keyExtractor={(item) => item?.email}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
/>
}
maxToRenderPerBatch={FETCH_RESULTS}
ListEmptyComponent={<Loader isLoading />}
initialScrollIndex={0}
// Layout doesn't know the exact location of the requested element.
// Falling back to calculating the destination manually
onScrollToIndexFailed={({ index, averageItemLength }) => {
flatListRef.current?.scrollToOffset({
offset: index * averageItemLength,
animated: true,
});
}}
/>
N.B., the ListEmptyComponent is used to render any component when the list is empty; in our case, we have provided a Loader component
You can customize a refresh control by providing certain properties. These properties allow you to modify the background color, text color, and other aspects of the loader offered by the component.
You can find a list of props that can be passed to the refresh control component here, but it’s important to bear in mind that certain properties will function exclusively on Android, while others will only work on iOS. For instance, the colors prop accepts a list of colors and will only work on Android, while the tintColor prop works only on iOS.
To provide a custom color for the refresh control for all use cases, you’ll need to combine both props:
// JavaScript
// screens/TopLoader.jsx
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
tintColor='red'
colors={['red', 'green', 'blue']}
// progressBackgroundColor={'green'}
// title='Loading users...'
// titleColor='green'
// size={'large'}
// progressViewOffset={200}
/>
Our application’s top loader screen with pull-to-refresh functionality and a new, custom refresh control is shown below:

The concept behind scroll-to-refresh is that when users reach the bottom of a list and continue dragging the list downward, it will automatically update by fetching the next set of results. To achieve this functionality, we use two specific props for the FlatList component: onEndReached and onEndReachedThreshold.
The onEndReached prop triggers when the user reaches a certain distance from the end of the list, as defined by the onEndReachedThreshold prop. This threshold value determines how far, in terms of visibility, the bottom edge of the list should be from the end of its content. When the threshold is set to zero, the onEndReached callback will be activated as soon as the user scrolls to the very bottom of the list.
For demonstration purposes, let’s open components/BottomLoader.jsx file and add the following code:
// JavaScript
// screens/BottomLoader.jsx
import { FlatList, StyleSheet, Text, View } from 'react-native';
import React, { useEffect, useRef, useState } from 'react';
import { FETCH_RESULTS, useFetchUser } from '../hooks/useFetchUser';
import { Item } from '../components/Item';
import { Loader } from '../components/Loader';
const BottomLoader = () => {
const { isLoading, success, users, errorMessage, getUsers } = useFetchUser();
const [currentPage, setCurrentPage] = useState(1);
const loadMoreItem = (e) => {
setCurrentPage((prev) => prev + 1);
};
useEffect(() => {
getUsers(currentPage);
}, [currentPage]);
return (
<View>
<Text
style={{
textAlign: 'center',
paddingVertical: 10,
fontSize: 18,
fontWeight: '600',
}}
>
Infinite Bottom Loader
</Text>
<FlatList
data={users}
renderItem={Item}
keyExtractor={(item) => item?.email}
maxToRenderPerBatch={FETCH_RESULTS}
ListEmptyComponent={<Loader isLoading />}
/>
{errorMessage ? <Text>{errorMessage}</Text> : null}
</View>
);
};
export default BottomLoader;
In the above code, we import the required dependencies and initiate the useFetchUser Hook to retrieve data. Then we establish the current page state and create a function called loadMoreItem, that increments the currentPage state.
The useEffect Hook observes changes in state of the current page and triggers the getUsers callback. We also include similar markup to what we provided for the TopLoader.
To implement the scroll-to-refresh functionality, we simply update our FlatList component with the below code:
// JavaScript
// screens/BottomLoader.jsx
<FlatList
data={users}
renderItem={Item}
keyExtractor={(item) => item?.email}
ListFooterComponent={<Loader isLoading={isLoading} />}
onEndReached={loadMoreItem}
onEndReachedThreshold={0}
maxToRenderPerBatch={FETCH_RESULTS}
ListEmptyComponent={<Loader isLoading />}
/>
N.B., in the above code the ListFooterComponent is used to indicate the loading state of the API requests, but can be any custom component of your choice
To test out what we’ve built, update the App.js file, like so:
// JavaScript
// App.js
import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import BottomLoader from './screens/BottomLoader';
const App = () => {
return (
<View style={styles.container}>
<BottomLoader />
</View>
);
};
export default App;
const styles = StyleSheet.create({
container: {
flex: 1,
marginVertical: 40,
marginHorizontal: 20,
},
});
Here’s our application, showing the scroll-to-refresh functionality in action:

Next, we’ll streamline the pull-to-refresh functionality and scroll-to-refresh functionality into a single FlatList component.
To start, open the screens/Combined.jsx file and import the required dependencies and components:
// JavaScript
// screens/Combined.jsx
import { FlatList, RefreshControl, StyleSheet, Text, View } from 'react-native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FETCH_RESULTS, useFetchUser } from '../hooks/useFetchUser';
import { Item } from '../components/Item';
import { Loader } from '../components/Loader';
Next, we create our component and call the useFetchUser Hook in two places for the top and bottom loaders, like so:
// JavaScript
// screens/Combined.jsx
const Combined = () => {
const flatListRef = useRef(null);
const {
isLoading: isLoadingTop,
success: successTop,
users: usersTop,
errorMessage: errorMessageTop,
getUsers: getUsersTop,
} = useFetchUser();
const {
isLoading: isLoadingBottom,
success: successBottom,
users: usersBottom,
errorMessage: errorMessageBottom,
getUsers: getUsersBottom,
} = useFetchUser();
return (
<View>
</View>
)
export default Combined;
In JavaScript, we can rename a destructured value by using a colon after the value:
const { isLoading } = useFetchUser();
// isLoading can be renamed to isLoadingTop
const { isLoading: isLoadingTop } = useFetchUser();
Now, we define the combinedUsers, currentPage, refreshing, and isTop states:
// JavaScript // screens/Combined.jsx const [combinedUsers, setConfirmedUsers] = useState([]); const [currentPage, setCurrentPage] = useState(1); const [refreshing, setRefreshing] = useState(false); const [isTop, setIsTop] = useState(false);
Then we define the loadMoreItem function:
// JavaScript
// screens/Combined.jsx
const loadMoreItem = () => {
setCurrentPage((prev) => prev + 1);
};
Next, we define the onRefresh callback and scrollToItem function:
// JavaScript
// screens/Combined.jsx
const onRefresh = useCallback(() => {
setRefreshing(true);
setIsTop(true);
loadMoreItem();
}, []);
const scrollToItem = (index) => {
flatListRef.current.scrollToIndex({ index: index, animated: false });
};
Then, we define the useEffect Hook to fetch the data:
// JavaScript
// screens/Combined.jsx
useEffect(() => {
if (isTop) {
getUsersTop(currentPage, isTop);
} else {
getUsersBottom(currentPage, isTop);
}
}, [currentPage]);
In the above code, we check for when the value of isTop changes in order to determine the function to call each time the currentPage state changes.
Then we define actions for when the pull-to-refresh and scroll-to-bottom API requests are successful:
// JavaScript
// screens/Combined.jsx
useEffect(() => {
if (successTop) {
setRefreshing(false);
setConfirmedUsers(usersTop);
if (combinedUsers.length > 0) {
scrollToItem(FETCH_RESULTS - 1);
}
}
}, [successTop]);
useEffect(() => {
if (successBottom) {
setConfirmedUsers(usersBottom);
}
}, [successBottom]);
Next, we combine the markup, like so:
// JavaScript
// screens/Combined.jsx
<View>
<Text
style={{
textAlign: 'center',
paddingVertical: 10,
fontSize: 18,
fontWeight: '600',
}}
>
Combined Bidirectional FlatList
</Text>
{errorMessageTop ? <Text>{errorMessageTop}</Text> : null}
<FlatList
ref={flatListRef}
data={combinedUsers}
renderItem={Item}
keyExtractor={(item) => item?.email}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
// tintColor='red'
// colors={['red', 'green', 'blue']}
// progressBackgroundColor={'green'}
// title='Loading users...'
// titleColor='green'
// size={'large'}
// progressViewOffset={200}
// tintColor='transparent'
// colors={['transparent']}
// style={{ backgroundColor: 'transparent' }}
/>
}
maxToRenderPerBatch={FETCH_RESULTS}
// ListHeaderComponent={<Loader isLoading={refreshing} withText={true} />}
ListFooterComponent={<Loader isLoading={isLoadingBottom} />}
onEndReached={() => {
loadMoreItem();
setIsTop(false);
}}
onEndReachedThreshold={0}
ListEmptyComponent={<Loader isLoading />}
initialScrollIndex={0}
// Layout doesn't know the exact location of the requested element.
// Falling back to calculating the destination manually
onScrollToIndexFailed={({ index, averageItemLength }) => {
flatListRef.current?.scrollToOffset({
offset: index * averageItemLength,
animated: true,
});
}}
/>
{errorMessageBottom ? <Text>{errorMessageBottom}</Text> : null}
</View>
Our component should look like this:
// JavaScript
// screens/Combined.jsx
import { FlatList, RefreshControl, StyleSheet, Text, View } from 'react-native';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { FETCH_RESULTS, useFetchUser } from '../hooks/useFetchUser';
import { Item } from '../components/Item';
import { Loader } from '../components/Loader';
const Combined = () => {
const flatListRef = useRef(null);
const {
isLoading: isLoadingTop,
success: successTop,
users: usersTop,
errorMessage: errorMessageTop,
getUsers: getUsersTop,
} = useFetchUser();
const {
isLoading: isLoadingBottom,
success: successBottom,
users: usersBottom,
errorMessage: errorMessageBottom,
getUsers: getUsersBottom,
} = useFetchUser();
const [combinedUsers, setConfirmedUsers] = useState([]);
const [currentPage, setCurrentPage] = useState(1);
const [refreshing, setRefreshing] = useState(false);
const [isTop, setIsTop] = useState(false);
const loadMoreItem = () => {
setCurrentPage((prev) => prev + 1);
};
const onRefresh = useCallback(() => {
setRefreshing(true);
setIsTop(true);
loadMoreItem();
}, []);
const scrollToItem = (index) => {
flatListRef.current.scrollToIndex({ index: index, animated: false });
};
useEffect(() => {
if (isTop) {
getUsersTop(currentPage, isTop);
} else {
getUsersBottom(currentPage, isTop);
}
}, [currentPage]);
useEffect(() => {
if (successTop) {
setRefreshing(false);
setConfirmedUsers(usersTop);
if (combinedUsers.length > 0) {
scrollToItem(FETCH_RESULTS - 1);
}
}
}, [successTop]);
useEffect(() => {
if (successBottom) {
setConfirmedUsers(usersBottom);
}
}, [successBottom]);
return (
<View>
<Text
style={{
textAlign: 'center',
paddingVertical: 10,
fontSize: 18,
fontWeight: '600',
}}
>
Combined Bidirectional FlatList
</Text>
{errorMessageTop ? <Text>{errorMessageTop}</Text> : null}
<FlatList
ref={flatListRef}
data={combinedUsers}
renderItem={Item}
keyExtractor={(item) => item?.email}
refreshControl={
<RefreshControl
refreshing={refreshing}
onRefresh={onRefresh}
// tintColor='red'
// colors={['red', 'green', 'blue']}
// progressBackgroundColor={'green'}
// title='Loading users...'
// titleColor='green'
// size={'large'}
// progressViewOffset={200}
// tintColor='transparent'
// colors={['transparent']}
// style={{ backgroundColor: 'transparent' }}
/>
}
maxToRenderPerBatch={FETCH_RESULTS}
// ListHeaderComponent={<Loader isLoading={refreshing} withText={true} />}
ListFooterComponent={<Loader isLoading={isLoadingBottom} />}
onEndReached={() => {
loadMoreItem();
setIsTop(false);
}}
onEndReachedThreshold={0}
ListEmptyComponent={<Loader isLoading />}
initialScrollIndex={0}
// Layout doesn't know the exact location of the requested element.
// Falling back to calculating the destination manually
onScrollToIndexFailed={({ index, averageItemLength }) => {
flatListRef.current?.scrollToOffset({
offset: index * averageItemLength,
animated: true,
});
}}
/>
{errorMessageBottom ? <Text>{errorMessageBottom}</Text> : null}
</View>
);
};
export default Combined;
const styles = StyleSheet.create({});
To see what we’ve built, update the App.js file, like so:
// JavaScript
// App.js
import { StyleSheet, Text, View } from 'react-native';
import React from 'react';
import Combined from './screens/Combined';
const App = () => {
return (
<View style={styles.container}>
<Combined />
</View>
);
};
export default App;
const styles = StyleSheet.create({
container: {
flex: 1,
marginVertical: 40,
marginHorizontal: 20,
},
});
Our final application should function like this:

In the article, we investigated how to implement pull-to-refresh and scroll-to-refresh and also how to combine these functionalities in a React Native project. We also discussed how to customize the refresh control component.
Thanks for reading! I hope you found this article useful for better understanding how to add pull-to-refresh and refresh control to your React Native application. Be sure to leave a comment if you have any questions. Happy coding!

LogRocket's Galileo AI watches sessions for you and and surfaces the technical and usability issues holding back your React Native apps.
LogRocket also helps you increase conversion rates and product usage by showing you exactly how users are interacting with your app. LogRocket's product analytics features surface the reasons why users don't complete a particular flow or don't adopt a new feature.
Start proactively monitoring your React Native apps — try LogRocket for free.

Discover what’s new in The Replay, LogRocket’s newsletter for dev and engineering leaders, in the November 5th issue.

A senior developer discusses how developer elitism breeds contempt and over-reliance on AI, and how you can avoid it in your own workplace.

Examine AgentKit, Open AI’s new tool for building agents. Conduct a side-by-side comparison with n8n by building AI agents with each tool.

AI agents powered by MCP are redefining interfaces, shifting from clicks to intelligent, context-aware conversations.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up now