Carl Rippon

Building SPAs

Carl Rippon
BlogBooks / CoursesAbout
This site uses cookies. Click here to find out more

Paging in React Query

March 24, 2021
reacttypescript

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.

Paging

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:

Janky paging

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:

Smooth paging

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.
Click here to share this post on Twitter

If you to learn more about using TypeScript with React, you may find my course useful:

Using TypeScript with React

Find out more