DEV Community

Cover image for How to combine SSR and pagination with react-query
Elisabeth Leonhardt
Elisabeth Leonhardt

Posted on • Edited on

How to combine SSR and pagination with react-query

If you read my latest post about pagination with react query, you might have noticed that everything was client-side rendered. That's fine for some cases, but in others, you might require server-side rendering for better speed or SEO. Today, I want to adapt the code we built last time to set up a server-side rendered pagination with Next.js and react-query:

1. Setting up the project

To not bore you with a new project setup, we will just modify the code from the previous article I wrote. Go ahead and clone the repository: you can inspect the finished code inside the PaginationSSR.js file in the pages directory or you copy the code from PaginationCSR.js inside a new page and follow along.

2. Evaluating two ways to obtain data on the server-side

As detailed in the react-query docs on SSR, there are two ways of passing data into your page:

a. using initialData

This is very easy: We just fetch the needed data on the server-side and give it to react-query as initalData and we are all set. There are some disadvantages though:

  • we won't know when exactly the data was fetched, it could be stale already
  • react-query won't know what exactly this initialData is. If you pass the data for the first page as initialData on the server-side, react-query will also fetch the same data on the client-side, adding an unnecessary API request.

b. using hydration

The mentioned issues are avoided using hydration, but the setup is a little more complex. However, I want to provide you with a solution that is bulletproof and production-ready, so we will go with option b.

3. Setting up Hydration

  • The first change has to be done in _app.js: We want to create the QueryClient inside of the app instead of outside. We also need to wrap our app inside an additional Hydrate component and pass in the dehydrated state as prop. The result should look like this:
import "../styles/globals.css";
import { ReactQueryDevtools } from "react-query/devtools";

import { Hydrate, QueryClient, QueryClientProvider } from "react-query";
import { useState } from "react";

function MyApp({ Component, pageProps }) {
  const [queryClient] = useState(() => new QueryClient());
  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
        <ReactQueryDevtools initialIsOpen={false}></ReactQueryDevtools>
      </Hydrate>
    </QueryClientProvider>
  );
}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode
  • Now, if you didn't do it yet, create a new file in the pages folder called paginationSSR.js and copy and paste all the code that is inside paginationCSR.js. Only change the name of the component and verify that everything is working as expected.
  • Let's start with the getServerSideProps function: We need to define a new QueryClient and make use of the prefetchQuery function. The result is returned as dehydratedState inside props to our page. Keep in mind that the query we write here has to have the same name and dependency array like the one inside the page component, otherwise, it will be treated as a prefetch for a non-existing query, and its data will be garbage-collected. The resulting code looks like this:
export async function getServerSideProps(context) {
  let page = 1;
  if (context.query.page) {
    page = parseInt(context.query.page);
  }
  const queryClient = new QueryClient();
  await queryClient.prefetchQuery(
    ["characters", page],
    async () =>
      await fetch(
        `https://rickandmortyapi.com/api/character/?page=${page}`
      ).then((result) => result.json()),
  );
  return { props: { dehydratedState: dehydrate(queryClient) }    };
}
Enter fullscreen mode Exit fullscreen mode
  • We are almost done! There are only some tiny adjustments left. On one hand, you will notice in the react-query devtools that when you enter localhost:3001/paginationSSR?page=14 to go directly to page 14 for example, will also fetch the data for page 1. This happens because our default value for page is set to 1, so it fetches the data for page 1 immediately after rendering. We will fix it like so:
const [page, setPage] = useState(parseInt(router.query.page) || 1);
Enter fullscreen mode Exit fullscreen mode

now you can delete the useEffect hook. Since this page is server-side rendered, it has access to the page parameter immediately.

  • last but not least, don't forget to change the base-URL inside the handlePaginationChange-function. Things can get very confusing when you test the server-side rendering and it suddenly redirects you to the client-side rendered version of the page... 🤦🏼‍♀️
  function handlePaginationChange(e, value) {
    setPage(value);
    router.push(`paginationSSR/?page=${value}`, undefined, { shallow: true });
  }
Enter fullscreen mode Exit fullscreen mode

some additional comments

  • react-query has some very aggressive defaults for refetching data, which are overkill for the application I am working with. This is why I set refetchonMount and refetchOnWindowFocus to false. You will have to evaluate your use case to see whether it's best to leave them activated.
  const { data } = useQuery(
    ["characters", page],
    async () =>
      await fetch(
        `https://rickandmortyapi.com/api/character/?page=${page}`
      ).then((result) => result.json()),
    {
      keepPreviousData: true,
      refetchOnMount: false,
      refetchOnWindowFocus: false,
    }
  );
Enter fullscreen mode Exit fullscreen mode
  • In a real application, it would be best to encapsulate the pagination component together with the grid into a separate component and reuse it, but this is meant to be a playground. However, always take a minute to think about code-reusability to make your future and your colleagues' life's easier. ❤️

That's it for today. Feel free to drop any questions in the comments section and have an amazing week!

Top comments (5)

Collapse
 
fabcodingzest profile image
Fab

YOU KNOW YOU SAVED ME
That refetchOnMount and window was really effing with me
THANK YOU

Collapse
 
adrien profile image
Adrien Rahier

I did not know you could use RQ with SSR. Thanks a lot for writing this article, super useful!

Collapse
 
frontenddeveli profile image
Elisabeth Leonhardt

Thank you so much! I am happy to know that it was useful! 🥰

Collapse
 
miklebr profile image
Mikhail Barulin

This is a nice mechanism provided by ReactQuery, but it has a twist. Actually we don't have SSR. RQ prefetch the data and sends it as a *cache * to the client. Because of this, we will not have markup from the server (And this is bad for SEO). RQ requests will also work on the first render, but the request data will be taken from the cache that the server sent us from dehydratedState

Collapse
 
ridham97 profile image
Ridham Sherathiya • Edited

Its a great help from this articles but i have one question is it also good in SEO?

Because first render from SSR and after that CSR so might me SEO will be not that much work...