Implementing Infinite Scroll with React Query and FlatList in React Native

Published on Jan 30, 2022

12 min read

EXPO

Originally published at Jscrambler.com

Infinite Scrolling is a way to implement pagination in mobile devices. It is common among mobile interfaces due to the limited amount of space. If you use social media applications like Instagram or Twitter, this implementation is commonly used across those apps.

In this tutorial, let's learn how to implement an infinite scroll using the FlatList component in React Native. To fetch data, we will use a real REST API service provided by RAWG. It is one of the largest video game databases, and they have a free tier when it comes to using their API for personal or hobby projects. React Query library will help us make the process of fetching data a lot smoother.

Prerequisites

🔗

To follow this tutorial, please make sure you have the following tools and utilities installed on your local development environment and have access to the services mentioned below:

  • Node.js version 12.x.x or above installed
  • Have access to one package manager such as npm or yarn or npx
  • RAWG API key

You can also check the complete source code for this example is at this GitHub repo.

Creating a new React Native app

🔗

To create a new React Native app, let's generate a project using create-react-native-app command-line tool. This tool helps create universal React Native apps, supports React Native Web, and you can use native modules. It is currently being maintained by the awesome Expo team.

Open up a terminal window and execute the following command:

npx create-react-native-app
# when prompted following questions
What is your app named? infinite-scroll-with-react-query
How would you like to start › Default new app
# navigate inside the project directory after it has been created
cd infinite-scroll-with-react-query

Then, let's install all the dependencies that will be used to create the demo app. In the same terminal window:

yarn add native-base react-query && expo install react-native-safe-area-context react-native-svg

This command should download all the required dependencies. To run the app in its vanilla state, you can execute either of the following commands (depending on the mobile OS you're using). These commands will build the app.

# for iOS
yarn ios
# for android
yarn android

Creating a Home Screen

🔗

Let's create a new directory called /src. This directory will contain all the code related to the demo app. Inside it, create a sub-directory called /screens that will contain the component file, HomeScreen.js.

In this file, let's add some JSX code to display the title of the app screen.

1import React from 'react';
2import { Box, Text, Divider } from 'native-base';
3
4export const HomeScreen = () => {
5 return (
6 <Box flex={1} safeAreaTop backgroundColor="white">
7 <Box height={16} justifyContent={'center'} px={2}>
8 <Text fontSize={28} fontWeight={'600'} color={'emerald.500'}>
9 Explore Games
10 </Text>
11 </Box>
12 <Divider />
13 </Box>
14 );
15};

The Box component from NativeBase is a generic component. It comes with many props, a few of them are to apply SafeAreaView of the device. The prop safeAreaTop applies padding from the top of the device's screen. One advantage of using the NativeBase library is its built-in components provide props like handling safe area views.

Most NativeBase components also use utility props for most commonly used styled properties such as justifyContent, backgroundColor, etc., and shorthands for these utility props such as px for padding horizontal.

Setting up providers

🔗

Both NativeBase and React Query libraries require their corresponding providers to set up at the root of the app. Open the App.js file and add the following:

1import React from 'react';
2import { StatusBar } from 'expo-status-bar';
3import { NativeBaseProvider } from 'native-base';
4import { QueryClient, QueryClientProvider } from 'react-query';
5
6import { HomeScreen } from './src/screens/HomeScreen';
7
8const queryClient = new QueryClient();
9
10export default function App() {
11 return (
12 <>
13 <StatusBar style="auto" />
14 <NativeBaseProvider>
15 <QueryClientProvider client={queryClient}>
16 <HomeScreen />
17 </QueryClientProvider>
18 </NativeBaseProvider>
19 </>
20 );
21}

All the providers must wrap the entry point or the first screen of the application. In the above snippet, there is only one screen, so all the providers are wrapping HomeScreen.

The QueryClientProvider component provides an instance in the form of QueryClient that can be further used to interact with the cache.

After modifying App.js, you will get the following output on a device:

ss1

Add a Base URL to use RAWG REST API

🔗

If you want to continue reading this post and build along with the demo app, make sure you have access to the API key for your RAWG account. Once you've done that, create a new file called index.js inside the /src/config directory. This file will export the base url of the API and API key.

1const BASE_URL = 'https://api.rawg.io/api';
2// Replace the Xs below with your own API key
3const API_KEY = 'XXXXXX';
4
5export { BASE_URL, API_KEY };

Replace the Xs in the above snippet with your own API key.

Fetching data from the API

🔗

To fetch the data, we will use JavaScript fetch API method. Create a new file called index.js inside /src/api. It will import the base url and the API key from the /config directory and expose a function that fetches the data.

1import { BASE_URL, API_KEY } from '../config';
2
3export const gamesApi = {
4 // later convert this url to infinite scrolling
5 fetchAllGames: () =>
6 fetch(`${BASE_URL}/games?key=${API_KEY}`).then(res => {
7 return res.json();
8 })
9};

Next, in the HomeScreen.js file, import React Query hook called useQuery. This hook accepts two arguments. The first argument is a unique key. This key is a unique identifier in the form of a string. It tracks the result of the query and caches it.

The second argument is a function that returns a promise. This promise is resolved when there is data or throws an error when there is something wrong when fetching the data. We've already created the promise function that fetches data asynchronously from the API's base Url in the form of gamesApi.fetchAllGames(). Let's import the gamesApi as well.

Inside the HomeScreen, let's call this hook to get the data.

1import React from 'react';
2import { Box, Text, FlatList, Divider, Spinner } from 'native-base';
3import { useQuery } from 'react-query';
4
5import { gamesApi } from '../api';
6
7export const HomeScreen = () => {
8 const { isLoading, data } = useQuery('games', gamesApi.fetchAllGames);
9
10 const gameItemExtractorKey = (item, index) => {
11 return index.toString();
12 };
13
14 const renderData = item => {
15 return (
16 <Text fontSize="20" py="2">
17 {item.item.name}
18 </Text>
19 );
20 };
21
22 return isLoading ? (
23 <Box
24 flex={1}
25 backgroundColor="white"
26 alignItems="center"
27 justifyContent="center"
28 >
29 <Spinner color="emerald.500" size="lg" />
30 </Box>
31 ) : (
32 <Box flex={1} safeAreaTop backgroundColor="white">
33 <Box height={16} justifyContent={'center'} px={2}>
34 <Text fontSize={28} fontWeight={'600'} color={'emerald.500'}>
35 Explore Games
36 </Text>
37 </Box>
38 <Divider />
39 <Box px={2}>
40 <FlatList
41 data={data.results}
42 keyExtractor={gameItemExtractorKey}
43 renderItem={renderData}
44 />
45 </Box>
46 </Box>
47 );
48};

In the above snippet, take a note that React Query comes with the implementation of request states such as isLoading. The isLoading state implies that there is no data and is currently in the "fetching" state. To improve the user experience, while the isLoading state is true, a loading indicator or a spinner component can be displayed (as did in the above snippet using the Spinner component from NativeBase).

Here is the output after this step:

ss2

Adding pagination to the API request

🔗

The useInfiniteQuery hook provided by the React Query library is a modified version of the useQuery hook. In addition to the request states such as isLoading and data, it utilizes a function to get the next page number using getNextPageParam.

In the case of RAWG REST API, the data fetch on each request contains the following keys:

  • count: the total count of games.
  • next: the url to the next page.
  • previous: the url of the previous page. Is null if the current page is first.
  • results: the array of items on an individual page.

The key names next, and previous will depend on the response structure of the API request. Make sure to check your data response what are the key names and what are their values.

Currently, the API request made in the /api/index.js file does not consider the number of the current page. Modify as shown below to fetch the data based on the page number.

1export const gamesApi = {
2 // later convert this url to infinite scrolling
3 fetchAllGames: ({ pageParam = 1 }) =>
4 fetch(`${BASE_URL}/games?key=${API_KEY}&page=${pageParam}`).then(res => {
5 return res.json();
6 })
7};

The addition &page=${pageParam} in the above snippet is how the getNextPageParam function will traverse to the next page if the current page number is passed in the request endpoint. Initially, the value of pageParam is 1.

Using useInfiniteQuery hook

🔗

Let's import the useInfiniteQuery hook in the HomeScreen.js file.

1// rest of the import statements remain same
2import { useInfiniteQuery } from 'react-query';

Next, inside the HomeScreen component, replace the useQuery hook with the useInfiniteQuery hook as shown below. Along with the two arguments, the new hook will also contain an object as the third argument. This object contains the logic to fetch the data from the next page using the getNextPageParam function.

The function retrieves the page number of the next page. It accepts a parameter called lastPage that contains the response of the last query. As per the response structure we discussed earlier in the previous section, check the value of lastPage.next. If it is not null, return the next page's number. If it is null, return the response from the last query.

1const { isLoading, data, hasNextPage, fetchNextPage } = useInfiniteQuery(
2 'games',
3 gamesApi.fetchAllGames,
4 {
5 getNextPageParam: lastPage => {
6 if (lastPage.next !== null) {
7 return lastPage.next;
8 }
9
10 return lastPage;
11 }
12 }
13);

Implementing infinite scroll on FlatList

🔗

In the previous snippet, the hasNextPage and fetchNextPage are essential. The hasNextPage contains a boolean. If it is true, it indicates that more data can be fetched. The fetchNextPage is the function provided by the useInfiniteQuery to fetch the data of the next page.

Add a handle method inside the HomeScreen component called loadMore. This function will be used on the FlatList prop called onEndReached. This prop is called when the scroll position reaches a threshold value.

1const loadMore = () => {
2 if (hasNextPage) {
3 fetchNextPage();
4 }
5};

Another difference between useInfiniteQuery and useQuery is that the former's response structure includes an array of fetched pages in the form of data.pages. Using JavaScript map function, get the results array of each page.

Modify the FlatList component as shown below:

1<FlatList
2 data={data.pages.map(page => page.results).flat()}
3 keyExtractor={gameItemExtractorKey}
4 renderItem={renderData}
5 onEndReached={loadMore}
6/>

Here is the output after this step. Notice the scroll indicator on the right-hand side of the screen. As soon as it reaches a little below half of the list, it repositions itself. This repositioning indicates that the data from the next page is fetched by the useInfiniteQuery hook.

ss3

The default value of the threshold is 0.5. This means that the loadMore will get triggered at the half-visible length of the list. To modify this value, you can add another prop, onEndReachedThreshold. It accepts a value between 0 and 1, where 0 is the end of the list.

1<FlatList
2 data={data.pages.map(page => page.results).flat()}
3 keyExtractor={gameItemExtractorKey}
4 renderItem={renderData}
5 onEndReached={loadMore}
6 onEndReachedThreshold={0.3}
7/>

Display a spinner when fetching next page data

🔗

Another way to enhance the user experience is when the end of the list is reached, and the data of the next page is still being fetched (let's say, the network is weak). While the app user waits for the data, it is good to display a loading indicator.

The useInfiniteQuery hook provides a state called isFetchingNextPage. Its value will be true when the data from the next page is fetched using fetchNextPage.

Modify the HomeScreen component as shown below. The loading spinner renders when the value of isFetchingNextPage is true. The ListFooterComponent on the FlatList component is used to display the loading indicator at the end of the list items.

1export const HomeScreen = () => {
2 const { isLoading, data, hasNextPage, fetchNextPage, isFetchingNextPage } =
3 useInfiniteQuery('games', gamesApi.fetchAllGames, {
4 getNextPageParam: lastPage => {
5 if (lastPage.next !== null) {
6 return lastPage.next;
7 }
8
9 return lastPage;
10 }
11 });
12
13 const loadMore = () => {
14 if (hasNextPage) {
15 fetchNextPage();
16 }
17 };
18
19 const renderSpinner = () => {
20 return <Spinner color="emerald.500" size="lg" />;
21 };
22
23 const gameItemExtractorKey = (item, index) => {
24 return index.toString();
25 };
26
27 const renderData = item => {
28 return (
29 <Box px={2} mb={8}>
30 <Text fontSize="20">{item.item.name}</Text>
31 </Box>
32 );
33 };
34
35 return isLoading ? (
36 <Box
37 flex={1}
38 backgroundColor="white"
39 alignItems="center"
40 justifyContent="center"
41 >
42 <Spinner color="emerald.500" size="lg" />
43 </Box>
44 ) : (
45 <Box flex={1} safeAreaTop backgroundColor="white">
46 <Box height={16} justifyContent={'center'} px={2}>
47 <Text fontSize={28} fontWeight={'600'} color={'emerald.500'}>
48 Explore Games
49 </Text>
50 </Box>
51 <Divider />
52 <Box px={2}>
53 <FlatList
54 data={data.pages.map(page => page.results).flat()}
55 keyExtractor={gameItemExtractorKey}
56 renderItem={renderData}
57 onEndReached={loadMore}
58 onEndReachedThreshold={0.3}
59 ListFooterComponent={isFetchingNextPage ? renderSpinner : null}
60 />
61 </Box>
62 </Box>
63 );
64};

Here is the output:

ss4

Wrapping up

🔗

In this tutorial, you've successfully implemented infinite scroll using useInfiniteQuery from React Query. Using this library for fetching and managing data inside a React Native app takes away a lot of pain points. Make sure to check out the Infinite Queries documentation here.

You can also check the complete source code for this example is at this GitHub repo.


More Posts

Browse all posts

Aman Mittal author

I'm a software developer and a technical writer. On this blog, I write about my learnings in software development and technical writing.

Currently, working maintaining docs at 𝝠 Expo. Read more about me on the About page.


Copyright ©  2019-2024 Aman Mittal · All Rights Reserved.