Tutorials

Implementing Infinite Scroll with React Query and FlatList in React Native

January 20th, 2022 | By Aman Mittal | 9 min read

Implementing Infinite Scrolling is a way to execute pagination on 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 use a REST API service provided by RAWG. It is one of the largest video game databases, and they have a free tier to use their API for personal or hobby projects.

Then the React Query library will help us make fetching data 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, yarn or npx

  • RAWG API key


You can also check out the complete source code at the GitHub repo with React Native examples.

Creating a new React Native app


To create a new React Native app, generate a project using the create-react-native-app command-line tool.

This tool helps create universal React Native apps, supports React Native Web, and allows you to use native modules.

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 we will use to create the demo app. In the same terminal window:

yarn add native-base react-query && expo install expo-linear-gradient 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 one of the following commands (depending on the mobile operating system). These commands will build the app.

# for iOS
yarn ios

# for android
yarn android


Creating a Home Screen


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 containing the component file, HomeScreen.js.

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

import React from 'react';
import { Box, Text, Divider } from 'native-base';

export const HomeScreen = () => {
  return (
    <Box flex={1} safeAreaTop backgroundColor='white'>
      <Box height={16} justifyContent={'center'} px={2}>
        <Text fontSize={28} fontWeight={'600'} color={'emerald.500'}>
          Explore Games
        </Text>
      </Box>
      <Divider />
    </Box>
  );
};


The Box component from NativeBase is generic. It comes with many props, and a few are applied to the SafeAreaView of the device.

The prop safeAreaTop applies padding from the top of the device's screen. One advantage of the NativeBase library is its built-in components, which provide props like handling safe area views.

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

Setting up providers

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

import React from 'react';
import { StatusBar } from 'expo-status-bar';
import { NativeBaseProvider } from 'native-base';
import { QueryClient, QueryClientProvider } from 'react-query';

import { HomeScreen } from './src/screens/HomeScreen';

const queryClient = new QueryClient();

export default function App() {
  return (
    <>
      <StatusBar style='auto' />
      <NativeBaseProvider>
        <QueryClientProvider client={queryClient}>
          <HomeScreen />
        </QueryClientProvider>
      </NativeBaseProvider>
    </>
  );
}


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 a QueryClient that can be further used to interact with the cache.

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

jscrambler-blog-implementing-flatlist-in-react-native

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 the API key.

const BASE_URL = 'https://api.rawg.io/api';
// Replace the Xs below with your own API key
const API_KEY = 'XXXXXX';

export { BASE_URL, API_KEY };


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

Fetching data from the API

To fetch the data, we will use the 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.

import { BASE_URL, API_KEY } from '../config';

export const gamesApi = {
  // later convert this url to infinite scrolling
  fetchAllGames: () =>
    fetch(`${BASE_URL}/games?key=${API_KEY}`).then(res => {
      return res.json();
    })
};


Next, in the HomeScreen.js file, import the React Query hook called useQuery. This hook accepts two arguments:

The first argument is a unique key: It is a unique identifier in the form of a string, and 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.

import React from 'react';
import { Box, Text, FlatList, Divider, Spinner } from 'native-base';
import { useQuery } from 'react-query';

import { gamesApi } from '../api';

export const HomeScreen = () => {
  const { isLoading, data } = useQuery('games', gamesApi.fetchAllGames);

  const gameItemExtractorKey = (item, index) => {
    return index.toString();
  };

  const renderData = item => {
    return (
      <Text fontSize='20' py='2'>
        {item.item.name}
      </Text>
    );
  };

  return isLoading ? (
    <Box
      flex={1}
      backgroundColor='white'
      alignItems='center'
      justifyContent='center'
    >
      <Spinner color='emerald.500' size='lg' />
    </Box>
  ) : (
    <Box flex={1} safeAreaTop backgroundColor='white'>
      <Box height={16} justifyContent={'center'} px={2}>
        <Text fontSize={28} fontWeight={'600'} color={'emerald.500'}>
          Explore Games
        </Text>
      </Box>
      <Divider />
      <Box px={2}>
        <FlatList
          data={data.results}
          keyExtractor={gameItemExtractorKey}
          renderItem={renderData}
        />
      </Box>
    </Box>
  );
};


In the above snippet, take 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 done in the above snippet using the Spinner component from NativeBase).

Here is the output after this step:

implementing infinite scroll


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 the 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 to see what the key names are and what their values are.

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.

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


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.

// rest of the import statements remain same
import { 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.

const { isLoading, data, hasNextPage, fetchNextPage } = useInfiniteQuery(
  'games',
  gamesApi.fetchAllGames,
  {
    getNextPageParam: lastPage => {
      if (lastPage.next !== null) {
        return lastPage.next;
      }

      return lastPage;
    }
  }
);


Implementing infinite scroll on FlatList

In the previous snippet, the hasNextPage and the fetchNextPage are essential.

The hasNextPage contains a boolean. If it is true, it indicates that more data can be fetched. The fetchNextPage function is provided by useInfiniteQuery to fetch the data for 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.

const loadMore = () => {
  if (hasNextPage) {
    fetchNextPage();
  }
};


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 the JavaScript map function, get the results array for each page.

Modify the FlatList component as shown below:

<FlatList
  data={data.pages.map(page => page.results).flat()}
  keyExtractor={gameItemExtractorKey}
  renderItem={renderData}
  onEndReached={loadMore}
/>


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.

implementing infinite scroll data fetch example

The default value of the threshold is 0.5. This means that 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.

<FlatList
  data={data.pages.map(page => page.results).flat()}
  keyExtractor={gameItemExtractorKey}
  renderItem={renderData}
  onEndReached={loadMore}
  onEndReachedThreshold={0.3}
/>


Display a spinner when fetching next page's 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.

export const HomeScreen = () => {
  const { isLoading, data, hasNextPage, fetchNextPage, isFetchingNextPage } =
    useInfiniteQuery('games', gamesApi.fetchAllGames, {
      getNextPageParam: lastPage => {
        if (lastPage.next !== null) {
          return lastPage.next;
        }

        return lastPage;
      }
    });

  const loadMore = () => {
    if (hasNextPage) {
      fetchNextPage();
    }
  };

  const renderSpinner = () => {
    return <Spinner color='emerald.500' size='lg' />;
  };

  const gameItemExtractorKey = (item, index) => {
    return index.toString();
  };

  const renderData = item => {
    return (
      <Box px={2} mb={8}>
        <Text fontSize='20'>{item.item.name}</Text>
      </Box>
    );
  };

  return isLoading ? (
    <Box
      flex={1}
      backgroundColor='white'
      alignItems='center'
      justifyContent='center'
    >
      <Spinner color='emerald.500' size='lg' />
    </Box>
  ) : (
    <Box flex={1} safeAreaTop backgroundColor='white'>
      <Box height={16} justifyContent={'center'} px={2}>
        <Text fontSize={28} fontWeight={'600'} color={'emerald.500'}>
          Explore Games
        </Text>
      </Box>
      <Divider />
      <Box px={2}>
        <FlatList
          data={data.pages.map(page => page.results).flat()}
          keyExtractor={gameItemExtractorKey}
          renderItem={renderData}
          onEndReached={loadMore}
          onEndReachedThreshold={0.3}
          ListFooterComponent={isFetchingNextPage ? renderSpinner : null}
        />
      </Box>
    </Box>
  );
};


Here is the output:

implementing infinite scroll final result

Wrapping Up


In this tutorial, you've successfully implemented infinite scrolling 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.

Protecting React Native apps

Finally, don't forget to pay special attention if you're developing commercial React Native apps that contain sensitive logic.

You can protect React Native apps against code theft, tampering, and reverse engineering.

Jscrambler

The leader in client-side Web security. With Jscrambler, JavaScript applications become self-defensive and capable of detecting and blocking client-side attacks like Magecart.

View All Articles

Must read next

Web Development

Add a Search Bar Using Hooks and FlatList in React Native

FlatList is a component of the React Native API that allows fetching and displaying large arrays of data. In this tutorial, we use it to add a search bar.

August 28, 2020 | By Aman Mittal | 8 min read

Web Development

Creating “Quarantine Pro” — A Fun Learning Experiment in React Native

Let's mix a bit of fun into learning React Native with a "quarantine score" app! It uses Expo font hook, Moment.js, and a date time picker modal.

May 14, 2020 | By Aman Mittal | 14 min read

Section Divider

Subscribe to Our Newsletter