Lazy Loading with React Query
This is another post in a series of posts using React Query with TypeScript.
Previous posts:
This post will cover how to use React Query to render a list that is lazily loaded. When the user scrolls to the bottom of the list, more data will be requested and rendered.
Query client provider
React Query requires a QueryClientProvider
component above the components using it:
import { QueryClient, QueryClientProvider } from "react-query";
...
const queryClient = new QueryClient();
render(
<QueryClientProvider client={queryClient}>
<App />
</QueryClientProvider>,
rootElement
);
We’ve added QueryClientProvider
at the top of the component tree to make React Query available to any component.
Fetching function
Our fetching function is as follows:
type CharactersPage = {
results: Character[];
next: number | undefined;
};
type Character = {
name: string;
};
async function getData({ pageParam = 1 }) {
const response = await fetch(
`https://swapi.dev/api/people/?page=${pageParam}`
);
if (!response.ok) {
throw new Error("Problem fetching data");
}
const dataFromServer = await response.json();
assertIsCharacterResponse(dataFromServer);
const data: CharactersPage = {
results: dataFromServer.results,
next: dataFromServer.next === null ? undefined : pageParam + 1,
};
return data;
}
The function takes in an object parameter containing a pageParam
property which is the page number being requested. pageParam
will be passed in as undefined
in the very first request, so this is defaulted to 1
.
The function makes a simple request and raises an error if unsuccessful.
The response data type is asserted to be CharacterResponse
with the assertIsCharacterResponse
type assert function, which we will look at a little later.
The response data is mapped into a slightly different object. The next
property is the next page number or undefined
if there is no next page.
Here is the the assertIsCharacterResponse
type assert function:
type CharacterResponse = {
results: Character[];
next: string;
};
function assertIsCharacterResponse(
response: any
): asserts response is CharacterResponse {
if (!("results" in response && "next" in response)) {
throw new Error("Not results");
}
if (response.results.length > 0) {
const firstResult = response.results[0];
if (!("name" in firstResult)) {
throw new Error("Not characters");
}
}
}
Query
In previous posts, we have used the useQuery
hook to call the fetching function and return useful state variables. There is another hook called useInfiniteQuery
which is useful when rendering a list in pages that are lazily loaded.
We can use useInfiniteQuery
as follows:
import { useInfiniteQuery } from "react-query";
export function App() {
const {
data,
error,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
status,
} = useInfiniteQuery<CharactersPage, Error>("characters", getData, {
getNextPageParam: (lastPage) => lastPage.next,
});
}
useInfiniteQuery
is very similar to useQuery
. We have passed in the following parameters:
- A key,
"characters"
, for the query. - The fetching function,
getData
. - A function that returns the next page. This is called to pass the next page number into the fetching function. The function takes in the current response data and returns the next page number.
We have destructured the following state variables:
data
: This contains all the pages of data in the follwoing structure:
{
pages: [
[arrayOfItemsInPage1],
[arrayOfItemsInPage2],
...
],
pageParams: [page1Number, page2Number, ...]
}
error
: The error object if an error has been raised.fetchNextPage
: The function to call to get the next page of data.hasNextPage
: Whether there is a next page.isFetchingNextPage
: Whether the next page is currently being requested.status
: The current status of the fetching process.
Rendering the list
The list is rendered as follows:
export function App() {
...
if (status === "loading") {
return <div>...</div>;
}
if (status === "error") {
return <div>{error!.message}</div>;
}
return (
<div>
<div>
{data &&
data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.results.map((character) => (
<div key={character.name}>{character.name}</div>
))}
</React.Fragment>
))}
</div>
<div>
{hasNextPage && (
<button
onClick={() => fetchNextPage()}
disabled={!hasNextPage || isFetchingNextPage}
>
{isFetchingNextPage ? "Loading ..." : "More"}
</button>
)}
</div>
</div>
);
}
The status
state is used to display a loading indicator and to display the error if it exists.
If there is data, all the pages of data are rendered. Remember, the data is structured into arrays of arrays where the root array are the pages, and the child arrays are the data within each page. So, we have to map through both these arrays.
We also render a More button that requests the next page of data when clicked.
Here’s what it looks like:
Loading when scrolled to the bottom of the list
This isn’t bad, but we can improve the user experience by automatically loading the next page when the user scrolls to the bottom of the list. So, the user will no longer have to click a More button to get more data - it will automatically appear.
We can make use of a package called react-infinite-scroll-component
to help us. We can install this by running the following command in a terminal:
npm install react-infinite-scroll-component
We use this as follows:
import InfiniteScroll from "react-infinite-scroll-component";
...
export function App() {
...
if (data === undefined) {
return null;
}
const dataLength = data.pages.reduce((counter, page) => {
return counter + page.results.length;
}, 0);
return (
<InfiniteScroll
dataLength={dataLength}
next={fetchNextPage}
hasMore={!!hasNextPage}
loader={<div>Loading...</div>}
>
{data.pages.map((group, i) => (
<React.Fragment key={i}>
{group.results.map((character) => (
<p key={character.name}>{character.name}</p>
))}
</React.Fragment>
))}
</InfiniteScroll>
);
}
We use the InfiniteScroll
component within the react-infinite-scroll-component
package and pass the following into it:
dataLength
: This is the number of items in the list across all pages. We use a reducer function to calculate this.next
: This is the function to fetch the next page of data.hasMore
: Whether there are more pages.!!
is used beforehasNextPage
to convert it to aboolean
withoutundefined
.loader
: This is a render prop to render a loading indicator when a page is being fetched.
Here’s the result:
Nice. 😊
The code in this post is available in CodeSandbox at https://codesandbox.io/s/react-query-lazy-loading-ttuc8?file=/src/App.tsx
Did you find this post useful?
Let me know by sharing it on Twitter.If you to learn more about using TypeScript with React, you may find my course useful: