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:
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 is a React Native monitoring solution that helps you reproduce issues instantly, prioritize bugs, and understand performance in 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.
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 nowCompare Prisma and Drizzle ORMs to learn their differences, strengths, and weaknesses for data access and migrations.
It’s easy for devs to default to JavaScript to fix every problem. Let’s use the RoLP to find simpler alternatives with HTML and CSS.
Learn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.