Welcome to this blog on implementing paginated and infinite queries with React Query!
To follow along, make sure you have a basic React project set up. You can create a new project with Vite or any other method you prefer. Additionally, we will be using Axios for API requests and React Query for data fetching (refer to the previous blog post for the basics of React Query).
Implementing Paginated Queries
Pagination is a common requirement in web applications, especially when dealing with large datasets. React Query is a powerful library for managing server state in your React applications, including handling paginated queries with ease.
Paginated Queries allow users to navigate through data using buttons to go to the previous or next pages. The client sends parameters to the server to specify which page of data it wants. Common parameters include the page number and the number of items per page. For example, a request might include page=2 and pageSize=10 to request the second page of data with 10 items per page.
Creating the Component
We will start by creating a simple component PostList.tsx
that will fetch and display a list of posts from a mock API 'JSONPlaceholder'.
Here we need a state variable page
to keep track of the current page and initialize it to 1. As the page size doesn't change, we don't need a state variable for that, we can declare a local constant pageSize
.
We need navigation buttons to go to the previous and next page. Previous page button should be disabled if we are on the first page.
import { useState } from 'react';
import usePosts from './hooks/usePosts';
const PostList = () => {
const pageSize = 10;
const [page, setPage] = useState(1);
const { data: posts, error, isLoading } = usePosts({ page, pageSize });
if (isLoading) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
return (
<>
<ul className="list-group">
{posts?.map((post) => (
<li key={post.id} className="list-group-item">
{post.title}
</li>
))}
</ul>
<button disabled={page === 1} onClick={() => setPage(page - 1)}>
Previous
</button>
<button onClick={() => setPage(page + 1)}>Next</button>
</>
);
};
export default PostList;
Creating the Query Hook
Now let's create a custom hook usePosts.ts
where we'll use the useQuery
hook from React Query to fetch the posts data.
Here we need to calculate the _start
and _limit
parameters based on the current page and page size, respectively. _start
is the index of our starting position, which we calculate by getting thequery.page
. We subtract it by one and multiply the result by page size. The second parameter is_limit
which we set to query.pageSize
.
import { useQuery } from '@tanstack/react-query';
import axios from 'axios';
interface Post {
id: number;
title: string;
}
interface PostQuery {
page: number;
pageSize: number;
}
const usePosts = (query: PostQuery) =>
useQuery<Post[], Error>({
queryKey: ['posts', query],
queryFn: () =>
axios
.get('https://jsonplaceholder.typicode.com/posts', {
params: {
_start: (query.page - 1) * query.pageSize,
_limit: query.pageSize,
},
})
.then((res) => res.data),
staleTime: 1 * 60 * 1000, //1m
});
export default usePosts;
Implementing Infinite Queries
Now let us see how to implement infinite queries in a React application using the useInfiniteQuery hook from the @tanstack/react-query
package.
Infinite Queries allows us to load data incrementally and handle pagination automatically, making our applications more efficient.
Example 1: Using RAWG API
Creating the Infinite Query Hook
First, let's create a custom hook called useGames
to fetch games from an API using Axios. This hook will use useInfiniteQuery
to handle pagination automatically.
To implement infinite queries, first, we should use useInfiniteQuery
instead of useQuery
. Next, we should have our query function(queryFn
) receive the page number as a parameter. For implementing Infinite Queries, I'm using RAWG API which supports data pagination using a query parameter called page. So we should pass the page
parameter to the backend and set it to pageParam
.
Next, we need to implement a function called getNextPageParam
which takes two parameters: lastPage
and allPages
and returns the next page number.
As allPages
contains the data for each page which we have retrieved. So, to compute the next page number we'll return allPages.length + 1
. Now at some point, we are going to hit the end of the list, so here we check if lastPage.next
is truthy we'll return allPages.length + 1
, otherwise, we'll return undefined
.
import { useInfiniteQuery } from '@tanstack/react-query';
import axios from 'axios';
interface FetchResponse<T> {
count: number;
next: string | null;
results: T[];
}
interface Game {
id: number;
name: string;
}
const useGames = () =>
useInfiniteQuery<FetchResponse<Game>, Error>({
queryKey: ['games'],
queryFn: ({ pageParam }) =>
axios
.get<FetchResponse<Game>>(
'https://api.rawg.io/api/games?key=***',
{
params: {
page: pageParam,
},
}
)
.then((res) => res.data),
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return lastPage.next ? allPages.length + 1 : undefined;
},
});
export default useGames;
Using the Infinite Query Hook in a Component
Next, let's use the useGames
hook in a component GameGrid.tsx
to display a list of games and a button to load more games.
Now here the data
object that we get from the infinite query is not an array of games, it's an instance of infinite data. In this object, we have a couple of properties. One is pages
which contains the data for all pages. So we go to data.pages
and map each page to a React Fragment
. Now inside this fragment, we map each page, which is an array of games, to a bunch of list items.
Now we want to render the button only if there is a next page. So here if hasNextPage
property is truthy then we render the button. Now we want to render the label dynamically. So here we can say if we are fetching the next page using property isFetchingNextPage
, we return 'Loading more', otherwise we return 'Load More'. Now finally we should handle the click event.
Now when the user clicks on the load more button, the fetchNextPage
function is called to get the next page number. At this point, React Query calls getNextPageParam
to calculate the next page number and then passes the page number to our query function queryFn
.
import React from 'react';
import useGames from './hooks/useGames';
const GameGrid = () => {
const {
data,
error,
isLoading,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = useGames();
if (isLoading) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
return (
<>
<ul>
{data?.pages.map((page, index) => (
<React.Fragment key={index}>
{page.results.map((game) => (
<li key={game.id}>{game.name}</li>
))}
</React.Fragment>
))}
</ul>
{hasNextPage && (
<button onClick={() => fetchNextPage()}>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
)}
</>
);
};
export default GameGrid;
Example 2: Using a mock API (e.g., JSONPlaceholder)
Now let's see how to implement Infinite Querie with the 'JSONPlaceholder' API.
Creating the Infinite Query Hook
import { useInfiniteQuery } from '@tanstack/react-query';
import axios from 'axios';
interface Post {
id: number;
title: string;
}
interface PostQuery {
pageSize: number;
}
const usePosts = (query: PostQuery) => {
return useInfiniteQuery<Post[], Error>({
queryKey: ['posts', query],
queryFn: ({ pageParam }) =>
axios
.get('https://jsonplaceholder.typicode.com/posts?', {
params: {
_start: ((pageParam as number) - 1) * query.pageSize,
_limit: query.pageSize,
},
})
.then((res) => res.data),
initialPageParam: 1,
getNextPageParam: (lastPage, allPages) => {
return lastPage.length > 0 ? allPages.length + 1 : undefined;
},
});
};
export default usePosts;
Using the Infinite Query Hook in a Component
import React from 'react';
import usePosts from './hooks/usePosts';
const PostList = () => {
const pageSize = 10;
const {
data,
error,
isLoading,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
} = usePosts({ pageSize });
if (isLoading) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
return (
<>
<ul>
{data?.pages.map((page, index) => (
<React.Fragment key={index}>
{page.map((post) => (
<li key={post.id}>
{post.title}
</li>
))}
</React.Fragment>
))}
</ul>
{hasNextPage && (
<button onClick={() => fetchNextPage()}>
{isFetchingNextPage
? 'Loading more...'
: hasNextPage
? 'Load More'
: 'Nothing more to load'}
</button>
)}
</>
);
};
export default PostList;
Implementing Infinite Scroll
Now let's make this even better by implementing infinite scroll. So as we scroll down the page, new games get fetched automatically. We'll implement infinite scroll using the popular library called react-infinite-scroll-component. So to install this library as below:
\>npm i react-infinite-scroll-component
Next, let's use 'Example 1' from above where we created the GameGrid
component and useGames
hook. The hook will remain the same, we'll make some modifications to the GameGrid
component.
So go to the GameGrid
component and wrap the list of games inside an <InfiniteScroll>
component. Now this component has several props.
dataLength: The first prop is
dataLength
, to specify the total number of items we have fetched so far, which we have to compute.For this purpose, we used a constant called
fetchedGamesCount
. So we have data.pages and we can use the reduce method to reduce this array of pages into a number that represents the total number of games we have fetched so far. So we know that each page contains an array of games. Here we are using the reduce method to combine the number of games on each page into a number. Initially, the data is undefined, so we should give this a default value of 0. Now we can use this to set the data length prop.hasMore: Next, we have the
hasMore
prop to indicate whether there are more items to fetch, we can set this tohasNextPage
which we got from our infinite query. But the type of this property is boolean or undefined. But this component expects a boolean. That is why we can apply double exclamations here to convert this to an actual boolean value. So if we have undefined here by applying double exclamations, undefined will be converted to boolean false.next: Next, we set the
next
prop. Here we pass a function and in this function, we callfetchNextPage()
. This is exactly like how we handled the click event of the load more button.loader and endMessage: Finally, we set
loader
andendMessage
props to respective text messages.
import React from 'react';
import useGames from './hooks/useGames';
import InfiniteScroll from 'react-infinite-scroll-component';
const GameGrid = () => {
const { data, error, isLoading, fetchNextPage, hasNextPage } = useGames();
if (isLoading) return <p>Loading...</p>;
if (error) return <p>{error.message}</p>;
const fetchedGamesCount =
data?.pages.reduce((total, page) => total + page.results.length, 0) || 0;
return (
<InfiniteScroll
dataLength={fetchedGamesCount}
// dataLength is the total no of items fetched so far
hasMore={!!hasNextPage}
// by applying !!, undefined value turns into false
next={() => fetchNextPage()}
loader={<p>Loading...</p>}
endMessage={<p>You have seen it all!</p>}
>
{/* Render your game items here */}
</InfiniteScroll>
);
};
export default GameGrid;
As the user scrolls down, more games will be fetched automatically using the fetchNextPage
function provided by React Query.
Conclusion
So we've learned how to implement paginated queries using React Query, which allows users to navigate through data using buttons to go to the previous and next pages.
We also learned how to implement infinite queries in a React application. This approach allows us to load data incrementally and handle pagination automatically, making our applications more efficient and responsive. Then we learned about Infinite Scroll which is a great way to improve user experience by dynamically loading content as the user scrolls.
I hope you found this helpful. Happy coding!