42
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:
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.
As detailed in the react-query docs on SSR, there are two ways of passing data into your page:
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.
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.
- 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;
- 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) } };
}
- 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);
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 hanldePaginationChange-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 });
}
- 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
andrefetchOnWindowFocus
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,
}
);
- 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!
42