DEV Community

Cover image for React Query v4 + SSR in Next JS
Arian Hamdi
Arian Hamdi

Posted on • Edited on

React Query v4 + SSR in Next JS

SSR data fetching + caching mechanism is a bit tricky in next js.

In this article, we will learn how to improve initial load time via SSR and have a high speed client side navigation with the help of CSR and React Query.

We will create a blog app using the JSON Placeholder API.

We are going to see just important sections here. To see the full source code checkout the github repo. You can also check the Live demo to get a better sight. The React Query devtools is available in this demo so you can check the cache flow.

Table of contents

1. Create a new project

First, create a nextjs project:

yarn create next-app blog-app

or

npx create-next-app blog-app
Enter fullscreen mode Exit fullscreen mode

Let's install React Query and Axios:

yarn add @tanstack/react-query axios

or

npm install @tanstack/react-query axios
Enter fullscreen mode Exit fullscreen mode

2. Setup Hydration

Due to the react query documents we set up hydration in _app.js :

//pages/_app.js

import { useState } from 'react';
import { Hydrate, QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { config } from 'lib/react-query-config';

function MyApp({ Component, pageProps }) {

    // This ensures that data is not shared 
    // between different users and requests
    const [queryClient] = useState(() => new QueryClient(config))

    return (
        <QueryClientProvider client={queryClient}>
            // Hydrate query cache
            <Hydrate state={pageProps.dehydratedState}>
                <Component  {...pageProps} />
            </Hydrate>
        </QueryClientProvider>
    )

}

export default MyApp;
Enter fullscreen mode Exit fullscreen mode

3. Prefetching and dehydrate data

Before we continue note that In v3, React Query would cache query results for a default of 5 minutes, then manually garbage collect that data. This default was applied to server-side React Query as well. This lead to high memory consumption and hanging processes waiting for this manual garbage collection to complete. In v4, by default the server-side cacheTime is now set to Infinity effectively disabling manual garbage collection (the NodeJS process will clear everything once a request is complete).

Now we need to prefetch data and dehydrate queryClient in getServerSideProps method :

//pages/posts/[id].js

import { getPost } from 'api/posts';
import { dehydrate, QueryClient } from '@tanstack/react-query';

export const getServerSideProps = async (ctx) => {

    const { id } = ctx.params;

    const queryClient = new QueryClient()

    // prefetch data on the server
    await queryClient.fetchQuery(['post', id], () => getPost(id))

    return {
        props: {
            // dehydrate query cache
            dehydratedState: dehydrate(queryClient),
        },
    }
}

Enter fullscreen mode Exit fullscreen mode

P.S : We used fetchQuery instead of prefetchQuery, because prefetchQuery doesn't throw any error or return any data. We will talk more about it in 6. handle 404 status code.

From now on we can easily use this prefetched data in our page, without passing any data by props.

Just to be clear lets take a look at the implementation of getPost method and usePost hook :

//api/posts.js

import axios from 'lib/axios';

export const getPost = async id => {
    const { data } = await axios.get('/posts/' + id);
    return data;
}
Enter fullscreen mode Exit fullscreen mode
//hooks/api/posts.js

import { useQuery } from '@tanstack/react-query';
import * as api from 'api/posts';

export const usePost = (id) => {
    return useQuery(['post', id], () => api.getPost(id));
}
Enter fullscreen mode Exit fullscreen mode

Now we can use this usePost hook to get post data.

//pages/posts/[id].js

import { useRouter } from 'next/router';
import { usePost } from 'hooks/api/posts'
import Loader from 'components/Loader';
import Post from 'components/Post';
import Pagination from 'components/Pagination';

const PostPage = () => {

    const { query: { id } } = useRouter();

    const { data, isLoading } = usePost(id);

    if (isLoading) return <Loader />

    return (
        <>
            <Post id={data.id} title={data.title} body={data.body} />
            <Pagination id={id} />
        </>
    )
}


// getServerSideProps implementation ...
// We talked about it in section 2
Enter fullscreen mode Exit fullscreen mode

4. Shallow Routing

We want to manage our data fetching and caching mechanism just in the client so we need to use shallow = true prop in the Link component for navigating between post pages to prevent calling getServerSideProps each time. This means that getServerSideProps method will only call when the users directly hit the URL of the post and not in the client side navigation within the app.

We have a Pagination component to navigate between pages, so we use shallow = true here :

//components/Pagination.jsx

import Link from 'next/link';

function PaginationItem({ index }) {

    return (
        <Link className={itemClassName} href={'/posts/' + index} shallow={true}>
            {index}
        </Link>
    )
}

export default PaginationItem;
Enter fullscreen mode Exit fullscreen mode

P.S : We used the new link component in nextjs v12.2 so we didn't need to use <a> tag here.

5. with-CSR HOC

At this time nextjs v12.2 shallow routing only works for URL changes in the current page. nextjs shallow routing caveats
this means if you navigate from /posts/10 to /posts/15 with shallow = true the getServerSideProps won't call but if you navigate from /home to /posts/15 the getServerSideProps is called even you use shallow routing and this will fetch unnecessary data even if it's available in the cache.

I found a work around that checks if this request to getServerSideProps is a client side navigation request or not. If it was, then returns an empty object for props and prevents fetching data on the server.
we can't prevent calling getServerSidePropswhen navigating between different pages but we can prevent fetching unnecessary data in the getServerSideProps.

Here is withCSR HOC implementation :

//HOC/with-CSR.js

export const withCSR = (next) => async (ctx) => {

    // check is it a client side navigation 
    const isCSR = ctx.req.url?.startsWith('/_next');

    if (isCSR) {
        return {
            props: {},
        };
    }

    return next?.(ctx)
}
Enter fullscreen mode Exit fullscreen mode

Now we should wrap our getServerSideProps with this HOC.

//pages/posts/[id].js

import { getPost } from 'api/posts';
import { dehydrate, QueryClient } from '@tanstack/react-query';
import { withCSR } from 'HOC/with-CSR'

export const getServerSideProps = withCSR(async (ctx) => {

    const { id } = ctx.params;

    const queryClient = new QueryClient()

    await queryClient.fetchQuery(['post', id], () => getPost(id))

    return {
        props: {
            dehydratedState: dehydrate(queryClient),
        },
    }
})
Enter fullscreen mode Exit fullscreen mode

If we navigate from different pages to post pages, getServerSideProps won't fetch any data and it just returns an empty object for props.

6. Handle 404 status code

While Next.js renders an error page if a post is not available, it doesn't actually respond with an error status code.

This means that while you can be viewing a 404 error, the page is actually responding with a 200 code. To search engines, this essentially translates to: "Everything went fine and we found the page". Rather than actually responding with a 404, which tells search engines that page doesn't exist.

To resolve this issue, let's take a look at getServerSideProps again :

const Page = ({ isError }) => {

    //show custom error component if there is an error
    if (isError) return <Error />

    return <PostPage />

}

export const getServerSideProps = withCSR(async (ctx) => {

    const { id } = ctx.params;

    const queryClient = new QueryClient();

    let isError = false;

    try {
        await queryClient.fetchQuery(['post', id], () => getPost(id));
    } catch (error) {
        isError = true
        ctx.res.statusCode = error.response.status;
    }

    return {
        props: {
            //also passing down isError state to show a custom error component.
            isError,
            dehydratedState: dehydrate(queryClient),
        },
    }
})

export default Page;
Enter fullscreen mode Exit fullscreen mode

7. Conclusion

We setup a caching mechanism with the ability to prefetch data on the server in SSR context. We also learned how to use shallow routing for faster client side navigation.

Here is the live demo of our implementation and the github repository for source code.
As well I had been added React Query devtools into production for you to understand thoroughly what is going under the hood.

I would like to extend my sincere thanks to @aly3n.

8. Refrences

  1. JSON Placeholder API
  2. React Query setup hydration
  3. React Query no manual garbage collection server side
  4. nextjs shallow routing caveats
  5. Prevent data fetching in getServerSideProps on client-side navigation
  6. respond with a 404 error in Next.js
  7. Project source code
  8. Live demo

Top comments (5)

Collapse
 
joostschuur profile image
Joost Schuur

I've been passing data fetched server-side (specifically on-demand ISR) into a regular React Query instance via as initialData on the client side, because I didn't know about its hydration support. Any particular benefits to using rehydration vs initialData?

Collapse
 
arianhamdi profile image
Arian Hamdi

There are a few tradeoffs to consider when compared to the hydration / dehydration approach:

  • If you are calling useQuery in a component deeper down in the tree you need to pass the initialData down to that point.

  • If you are calling useQuery with the same query in multiple locations, you need to pass initialData to all of them.

  • There is no way to know at what time the query was fetched on the server, so dataUpdatedAt and determining if the query needs refetching is based on when the page loaded instead.

For more information please check here.

Collapse
 
joostschuur profile image
Joost Schuur

I've just migrated over to the de/hydrate based approach, and so far, so good!

Thanks for the pointer to dataUpdatedAt too!

Collapse
 
juxtasolutions profile image
Steven Christopher

This post was awesome !! Huge help, thank you !!

Collapse
 
lunatix01 profile image
LunatiX

in my situation if i want to get response code of 429 return notFound: true
how can i do that?