React Native Infinite Scrolling with React Query

Joseph Odunsi
Level Up Coding
Published in
8 min readSep 30, 2022

--

HD photo by Mediamodifier

Infinite scrolling is a technique that constantly loads data as the user scrolls down the page, removing the need for pagination.

This tutorial will use React Native to create infinite scrolling and FlashList to render the data. We'll use the Dogs API to retrieve data about dogs, and we will utilize React Query to handle data processing.

Why FlashList?

FlashList provides much-improved speed, making your lists smooth with no blank cells. It "recycles components under the hood to maximize performance." Visit their website to learn more.

Initialize a React Native Project

We'll use Expo to create a new app. So, make sure you have Expo CLI working on your development machine Expo Go app is installed on your iOS or Android physical device or emulator. Check their installation guide if you haven't.

Run the following command in your project directory

npx expo init

You'll be prompted with some options

  1. What would you like to name your app? Enter any name you want. I'll go with rn-infinite-scroll
  2. Choose a template Choose blank

Now, wait until it is done installing the dependencies, navigate to the directory and start the app.

cd rn-infinite-scroll
yarn start

Installing dependencies

Stop the app and install the following dependencies.

yarn add @tanstack/react-query 
npx expo install @shopify/flash-list react-native-safe-area-context

Start the app again using yarn start

Setup React Query

We need to wrap the app with react query's provider to have access to the client

Open the App.js file and modify it as follows

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { StatusBar } from "expo-status-bar";
import { StyleSheet, Text, View } from "react-native";
// react query client instance
const queryClient = new QueryClient();
export default function App() {
return (
// react query provider
<QueryClientProvider client={queryClient}>
<View style={styles.container}>
<Text>Open up App.js to start working on your app!</Text>
<StatusBar style="auto" />
</View>
</QueryClientProvider>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
backgroundColor: "#fff",
},
});

Creating Screens

DogCard Component

Let's start by creating the home screen. In the project directory, create a new folder named components and create a new file called DogCard.js i.e./components/DogCard.js. Add the following code to it.

import React from "react";
import { Image, StyleSheet, Text, View } from "react-native";
const DogCard = ({ dog }) => {
return (
<View>
<View style={styles.row}>
<Image source={{ uri: dog.image.url }} style={styles.pic} />
<View style={styles.nameContainer}>
<Text style={styles.nameTxt}>{dog.name}</Text>
</View>
</View>
</View>
);
};
const styles = StyleSheet.create({
row: {
flexDirection: "row",
alignItems: "center",
borderColor: "#DCDCDC",
backgroundColor: "#fff",
borderBottomWidth: 1,
padding: 10,
},
pic: {
borderRadius: 30,
width: 60,
height: 60,
},
nameContainer: {
flexDirection: "row",
justifyContent: "space-between",
},
nameTxt: {
marginLeft: 15,
fontWeight: "600",
color: "#222",
fontSize: 18,
},
});
export default DogCard;

Here, we will be rendering the dog's breed name and image.

Home Component

Next, we create another folder in the root directory named screens and a new file called Home.js. Add the following to it.

import { FlashList } from "@shopify/flash-list";
import React from "react";
import { Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
const Home = () => {
return (
<SafeAreaView>
<FlashList
data={[{ name: "Dogs" }, { name: "Fido" }, { name: "Spot"}]}
renderItem={({ item }) => <Text>{item.name}</Text>}
estimatedItemSize={100}
/>
</SafeAreaView>
);
};
export default Home;

For now, we are using hardcoded data to render the list.

This is what your screen should be showing now

Fetching data

Next, we will handle fetching data from the Dogs API. Create a new folder in the root directory named hooks i.e. /hooks and a file, useFetchDogs i.e /hooks/useFetchDogs. This will be a custom hook to fetch our data. Populate the file with the following

import { useQuery } from "@tanstack/react-query";export default function useFetchDogs() {
const getDogs = async () => {
const res = await (
await fetch(`https://api.thedogapi.com/v1/breeds`)
).json();
return res;
};
return useQuery(["dogs"], getDogs);
}

The useFetchDogs is a custom hook. We created the getDogs function to fetch all the data, no pagination yet. And then return the result of useQuery the hook, which is imported from react-query to fetch the data. This hook takes at least two arguments:

  • A unique key for the query is used internally for re-fetching, caching, and sharing your queries throughout your application. In our case, the key is dogs.
  • A function that returns a promise which is the getDogs function in our case.

Now, let's modify the Home.js screen to display the data we are getting from the API.

// previous code here
import DogCard from "../components/DogCard";
import useFetchDogs from "../hooks/useFetchDogs";
const Home = () => {
const { data, isLoading, isError } = useFetchDogs();
if (isLoading) return <Text>Loading...</Text>; if (isError) return <Text>An error occurred while fetching data</Text>; return (
<SafeAreaView style={{ flex: 1, backgroundColor: "#fff" }}>
<FlashList
keyExtractor={(item) => item.id}
data={data}
renderItem={({ item }) => <DogCard dog={item} />}
estimatedItemSize={100}
/>
</SafeAreaView>
);
};
// remaining code here

We imported our custom hook and called it. The hook returns an object and extracts the data we'll use by destructuring. data contains the data we need to render. isLoading indicates that the data is being fetched, returning a boolean. And isError indicates if there's an error and also returns a boolean.

While isLoading is true, we return a text to let the user know, and while isError is true, we'll let the user know there's an error.

Also, note we are now using the DogCard component.

Data fetched with useQuery

Paginate with useInfiniteQuery

Right now, we are fetching all the data at once. That's not what we want. We want to fetch 10 of the data for every page.

To do that, we need to use a version of useQuery called useInfiniteQuery. We'll modify our custom hook first as follows

import { useInfiniteQuery } from "@tanstack/react-query";export default function useFetchDogs() {
const getDogs = async ({ pageParam = 0 }) => {
const res = await (
await fetch(
`https://api.thedogapi.com/v1/breeds?limit=10&page=${pageParam}`
)
).json();
return {
data: res,
nextPage: pageParam + 1,
};
};
return useInfiniteQuery(["dogs"], getDogs, {
getNextPageParam: (lastPage) => {
if (lastPage.data.length < 10) return undefined;
return lastPage.nextPage;
},
});
}

We are now using useInfiniteQuery instead of useQuery .

The getDogs function has a parameter, pageParam which is 0 by default. This means we want to start fetching from the first page. We appended the API URL with ?limit=10&page={pageParam} to get ten items from each page. We are also now returning modified data, an object with two keys, data and nextPage. After every successful API call, we increment nextPage. useInfiniteQuery needs the data in this format.

We don't have to do this for some APIs as they already have their data returned in a similar format. But for the Dogs API, we need to return the data in the structure ourselves for the useInfiniteQuery.

Next is the useInfiniteQuery returned. Just like useQuery but a third argument is an object with various optional keys. We added the getNextPageParam option to determine if there is more data to load and information to fetch.

getNextPageParam accepts a parameter, lastPage which contains the response we returned from getDogs function and returns the next page. We also added a condition to check if we've reached the last page.

Render data for Infinite Scrolling

First, let's examine the data format returned by useInfiniteQuery. The data is now an object with two keys:

  • pageParams - an array of the page number
  • pages - an array of our data for each page

We need to flatten the pages to join the data for each page as one array to map through it and render them. We can utilize JavaScript flatMap.

const flattenData = data.pages.flatMap((page) => page.data)

Let's modify our list component to render the flattened data. Open up Home.js

const Home = () => {
// previous code here
const flattenData = data.pages.flatMap((page) => page.data); return (
<SafeAreaView style={{ flex: 1, backgroundColor: "#fff" }}>
<FlashList
keyExtractor={(item) => item.id}
data={flattenData}
renderItem={({ item }) => <DogCard dog={item} />}
estimatedItemSize={100}
/>
</SafeAreaView>
);
};
export default Home;

Infinite Scroll with FlashList

We need to load extra data when the scroll position hits a certain threshold. We call a function to load more data when it hits the threshold value. The threshold value in React Native is between 0 and 1, with 0.5 being the default. That is, anytime the end of the content is within half the apparent length of the list, the function to fetch new data will be called.

useInfiniteQuery provides us with some other functions from its result when called. fetchNextPage and hasNextPage are now available. Let's create the function to load the next page's data.

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

We add a new prop FlashList to call the function when the scroll position gets within onEndReachedThreshold the rendered content.

<FlashList
keyExtractor={(item) => item.id}
data={flattenData}
renderItem={({ item }) => <DogCard dog={item} />}
onEndReached={loadNextPageData}
/>

Home.js should look like this:

import { FlashList } from "@shopify/flash-list";
import React from "react";
import { Text, View } from "react-native";
import { SafeAreaView } from "react-native-safe-area-context";
import DogCard from "../components/DogCard";
import useFetchDogs from "../hooks/useFetchDogs";
const Home = () => {
const { data, isLoading, isError, hasNextPage, fetchNextPage } =
useFetchDogs();
if (isLoading) return <Text>Loading...</Text>; if (isError) return <Text>An error occurred while fetching data</Text>; const flattenData = data.pages.flatMap((page) => page.data); const loadNext = () => {
if (hasNextPage) {
fetchNextPage();
}
};
return (
<SafeAreaView style={{ flex: 1, backgroundColor: "#fff" }}>
<FlashList
keyExtractor={(item) => item.id}
data={flattenData}
renderItem={({ item }) => <DogCard dog={item} />}
onEndReached={loadNext}
estimatedItemSize={100}
/>
</SafeAreaView>
);
};
export default Home;

Remember, the threshold value can be between 0 and 1. Change the it from the default value(0.5) to 0.2

<FlashList
keyExtractor={(item) => item.id}
data={flattenData}
renderItem={({ item }) => (
<Text style={{ height: 50 }}>{item.name}</Text>
)}
onEndReached={loadNext}
onEndReachedThreshold={0.2}
estimatedItemSize={100}
/>
Infinite Scrolling with useInfiniteQuery

Conclusion

We implement infinite scrolling with React Native, React Query's useInfiniteQuery and FlashList, consuming the data from Dogs API.

GitHub code here.

Try out yourself

  • Show a loading spinner while data is being fetched
  • Implement pull to refresh

Level Up Coding

Thanks for being a part of our community! Before you go:

🚀👉 Placing developers like you at top startups and tech companies

--

--