Paging in React Query
This is the third post in a series of posts using React Query with TypeScript.
Previous posts:
This post will cover how to page through a collection of Star Wars characters with React Query, providing a smooth user experience.
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:
async function getData(params: { queryKey: [string, { page: number }] }) {
const [, { page }] = params.queryKey;
const response = await fetch(`https://swapi.dev/api/people/?page=${page}`);
if (!response.ok) {
throw new Error("Problem fetching data");
}
const data = await response.json();
assertIsCharacterResponse(data);
return data;
}
The query key is passed into the function, which is a tuple with the second element containing the page number of data to request.
The function makes a simple request and raises an error if unsuccessful.
The type of the data is asserted to be CharacterResponse
with the assertIsCharacterResponse
type assert function:
type CharacterResponse = {
results: Character[];
next: string;
previous: string;
};
type Character = {
name: string;
};
function assertIsCharacterResponse(
response: any
): asserts response is CharacterResponse {
if (
!("results" in response && "next" in response && "previous" 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
The useQuery
hook can be used in the React component as follows to call the fetching function:
import { useQuery } from "react-query";
...
export function App() {
const [page, setPage] = React.useState(1);
const { status, error, data } = useQuery<
CharacterResponse,
Error
>(["characters", { page }], getData);
...
}
The page number is stored in state and initialised to the first page. The page number is then passed into the fetching function.
We have destructured the usual state variables from useQuery
to render different elements in different parts of the fetching process.
Rendering the list
The rendering inside the component is as follows:
export function App() {
...
if (status === "loading") {
return <div>...</div>;
}
if (status === "error") {
return <div>{error!.message}</div>;
}
if (data === undefined) {
return null;
}
return (
<div>
<div>
<button
disabled={page <= 1}
onClick={() => setPage((p) => p - 1)}
>
Previous
</button>
<span>Page: {page}</span>
<button
disabled={!data.next}
onClick={() => setPage((p) => p + 1)}
>
Next
</button>
</div>
{
<div>
{data.results.map((d) => (
<div key={d.name}>{d.name}</div>
))}
</div>
}
</div>
);
}
When the data is loaded, previous and next paging buttons are rendered along with the page of data.
This functions correctly but the paging is a bit janky:
keepPreviousData
There is an option in the useQuery
hook called keepPreviousData
which allows the previous data to be kept in place until the data from a new request replaces it. We can add this as follows:
const { status, error, data } = useQuery<
CharacterResponse,
Error
>(["characters", { page }], getData, { \
keepPreviousData: true });
There is also a isPreviousData
state variable that can be destructured:
const {
status,
error,
data,
isPreviousData,} = useQuery<CharacterResponse, Error>(
["characters", { page }],
getData,
{ keepPreviousData: true }
);
This tells us whether the render will be using previous data or not.
We can now tweek the pager buttons as follows:
<div>
<button
disabled={isPreviousData || page <= 1} onClick={() => setPage((p) => p - 1)}
>
Previous
</button>
<span>Page: {page}</span>
<button
disabled={isPreviousData || !data.next} onClick={() => setPage((old) => old + 1)}
>
Next
</button>
</div>
This disables the buttons when data is being fetched.
We now see that the paging is much smoother:
Nice. 😊
The code in this post is available in CodeSandbox at https://codesandbox.io/s/react-query-paging-trxxk?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: