Are you interested in using React Query in your Next.js 13 app directory? If so, you’ve come to the right place! In this article, I’ll guide you through the process of setting up React Query and making the QueryClient accessible to all components in the component tree.

Firstly, I’ll show you how to wrap the QueryClientProvider around the Children Node of the root layout component in the Next.js 13 app directory. This is important as it will ensure that all components in your app have access to the same query client.

You’ll then learn how to fetch initial data in a Server Component higher up in the component tree and pass it as a prop to your Client Component. Additionally, I’ll explain how to prefetch the query on the server, dehydrate the cache, and rehydrate it on the client.

Since Next.js 13 is still in beta and libraries are still being adjusted to work with it, React Query has faced some challenges when integrating with the Next.js 13 app directory. However, I have carefully reviewed all the solutions proposed by the React Query team on GitHub, and in this article, I’ll show you the right way to set up React Query in the new app directory of Next.js 13.

On July 15th, 2023, while scrolling through Twitter, I came across a tweet from the React Query team announcing a new package. This package makes working with React Query in the Next.js App Router much easier. Excited about this update, I have now included the recommended approach in the article.

More practice:

How to Setup React Query in Next.js 13 App Directory

Setup the Next.js 13 Project

After following this tutorial, your folder and file structure will look like the one shown in the screenshot below.

Folder Structure of the React Query Next.js 13 App Directory Project

In this tutorial, we will create a simple project that includes a counter app and a component for displaying a list of users. This project will showcase how React Query can be used in both server-side and client-side rendered components.

Using Redux Toolkit and RTK Query in Next.js App Directory

Now that we have an understanding of what we’ll be building, let’s get started by generating a Next.js 13 project. Navigate to any directory on your computer and open a terminal in that directory. Depending on your preferred package manager, you can run the following command to initiate the project scaffolding process.


yarn create next-app nextjs13-react-query
# or 
npx create-next-app@latest nextjs13-react-query

During the process, you’ll be prompted to choose which features to enable for the project. Make sure to select “Yes” for TypeScript and ESLint. Additionally, you’ll be asked if you’d like to use the src/ directory and the experimental app/ directory. Select “Yes” for both options, and for the import alias, simply press Tab and Enter to accept.

Once you’ve answered all the questions, the Next.js 13 project will be generated, and all necessary dependencies will be installed. After everything is installed, you can open the project in your preferred IDE or text editor, such as VS Code.

To proceed, replace the existing content of the src/app/page.tsx file with the code below, which contains links to different pages demonstrating all the workarounds for React Query in the new Next.js 13 app directory:

src/app/page.tsx


import Link from "next/link";

export default function Home() {
  return (
    <div>
      <h1>Hello, Next.js 13 App Directory!</h1>
      <p>
        <Link href="/hydration-stream-suspense">
          (recommended method): React Query With Streamed Hydration --- Bad for
          SEO
        </Link>
      </p>
      <p>
        <Link href="/initial-data">
          Prefetching Using initial data --- Good for SEO
        </Link>
      </p>
      <p>
        <Link href="/hydration">
          Prefetching Using Hydration --- Good for SEO
        </Link>
      </p>
    </div>
  );
}

Save the file, and upon visiting http://localhost:3000, you will see a preview resembling the one shown in the screenshot below:

The Home Page with Links to the Various Examples Of Using React Query In the New Next.js 13 App Directory

Recommended Approach: Using Client Stream Hydration

When I first wrote this article, there was no recommended way to use React Query in the new Next.js 13 app directory. However, the community came up with some workarounds, which involve handling the hydration yourself or passing the initial data to a client component from a server component. Fortunately, the React Query team has recently published an experimental package that makes working with React Query in the Next.js 13 App Router a breeze.

With this new experimental package, you do not need to manually handle hydration or pass the initialData as a prop from a server component to a client component. When the initial request is made, the data will be fetched on the server, which means the API request from the useQuery hook is first initialized on the server to fetch the data. Once the data is ready, it will be automatically made available in the QueryClient on the client.

To install the package, run the following command:


# For yarn
yarn add @tanstack/react-query-next-experimental

# For PNPM
pnpm add @tanstack/react-query-next-experimental

# For NPM
npm i @tanstack/react-query-next-experimental

Create a Client Query Client Provider

To use the installed package, we need to wrap the ReactQueryStreamedHydration and QueryClientProvider components around the entry point of our Next.js application. However, since all components within the app directory are server components by default, attempting to do so will result in errors because the QueryClientProvider can only be initialized on the client.

To resolve this, we must create a client-side provider where we can initialize the components. Then, we can safely wrap the client-side provider around the root children without encountering any errors.

To begin, let’s create a utils directory within the src directory. Within the utils folder, create a provider.tsx file and insert the following code:

src/utils/provider.tsx


"use client";

import React from "react";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";
import { ReactQueryStreamedHydration } from "@tanstack/react-query-next-experimental";

function Providers({ children }: React.PropsWithChildren) {
  const [client] = React.useState(new QueryClient());

  return (
    <QueryClientProvider client={client}>
      <ReactQueryStreamedHydration>{children}</ReactQueryStreamedHydration>
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

export default Providers;

The ReactQueryStreamedHydration component from the experimental package enables the streaming of data from the server to the client.

Wrap the Provider Around the Root Children Prop

Next, we need to wrap the client-side provider we created earlier around the children prop of the root layout component. This ensures that the QueryClient is made available to all the client-side components within the app. To do this, open the src/app/layout.tsx file and make the following modifications:

src/app/layout.tsx


import Providers from "@/utils/provider";
import React from "react";
// import "./globals.css";

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Use React Query for Data Fetching

Now we can start using useQuery from React Query to fetch data in our Next.js app. The only requirement is to set suspense: true in the useQuery options for this recommended approach to work. Here’s an example of how to use useQuery to fetch a list of users and display them in the UI:

src/app/hydration-stream-suspense/list-users.tsx


"use client";

import { User } from "../types";
import { useQuery } from "@tanstack/react-query";
import React from "react";

async function getUsers() {
  return (await fetch("https://jsonplaceholder.typicode.com/users").then(
    (res) => res.json()
  )) as User[];
}

export default function ListUsers() {
  const [count, setCount] = React.useState(0);
  const { data } = useQuery<User[]>({
    queryKey: ["stream-hydrate-users"],
    queryFn: () => getUsers(),
    suspense: true,
    staleTime: 5 * 1000,
  });

  React.useEffect(() => {
    const intervalId = setInterval(() => {
      setCount((prev) => prev + 1);
    }, 100);

    return () => {
      clearInterval(intervalId);
    };
  }, []);

  return (
    <>
      <p>{count}</p>
      {
        <div
          style={{
            display: "grid",
            gridTemplateColumns: "1fr 1fr 1fr 1fr",
            gap: 20,
          }}
        >
          {data?.map((user) => (
            <div
              key={user.id}
              style={{ border: "1px solid #ccc", textAlign: "center" }}
            >
              <img
                src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
                alt={user.name}
                style={{ width: 180, height: 180 }}
              />
              <h3>{user.name}</h3>
            </div>
          ))}
        </div>
      }
    </>
  );
}

Wrap the Client Component in React Suspense

To render the ListUsers component we created earlier, we need to include it in the JSX of a page file. Since we included suspense: true in the useQuery options, we must wrap the <Suspense> component from React around the ListUsers component in order for the streaming of data from the server to the client to work.

src/app/hydration-stream-suspense/page.tsx


import Counter from "./counter";
import ListUsers from "./list-users";
import { Suspense } from "react";

export default async function Page() {
  return (
    <main style={{ maxWidth: 1200, marginInline: "auto", padding: 20 }}>
      <Counter />
      <Suspense
        fallback={
          <p style={{ textAlign: "center" }}>loading... on initial request</p>
        }
      >
        <ListUsers />
      </Suspense>
    </main>
  );
}

If you want to see React Query in action in the new Next.js app directory, you can clone the project from https://github.com/wpcodevo/nextjs13-react-query and open it in your text editor. Take a look at the code or start the Next.js development server and open the application in your browser to test the different ways of using React Query in Next.js 13.

Data is now fetched on the client on subsequent client-side navigation

To verify if the data is initially fetched on the server, manually refresh the browser and check the terminal where the dev server is running. You should see the API request logged in the terminal, confirming that the data is being fetched on the server.

See the logs of the initial request made by the useQuery hook on the server

Because we are using Suspense boundaries to achieve data streaming from the server to the client, when you inspect the document in your browser’s dev tools, you will only see the loading component used in the fallback function. This is not good for SEO.

Using React Query in the New Next.js 13 App Directory Workaround Which Involves Suspense Boundary that is Bad for SEO

As of now, this is one of the drawbacks of using the @tanstack/react-query-next-experimental package. Nevertheless, it’s expected that future releases will address the SEO compatibility issue. In the meantime, if you require improved SEO, you can consider implementing the workarounds provided below.

Previous WorkArounds

The content within this section describes the previous ways we used React Query in the app directory. These approaches involved using tricks to make hydration work or passing initial data from a server component to a client component.

Create a Custom Query Client Provider

By default, all components in Next.js 13 are rendered on the server but React Query doesn’t work with Server Components. Therefore, we must create a custom provider to render the QueryClientProvider within a Client Component.

To ensure that the custom provider component only renders on the client-side, we can add "use client"; at the top of the file. This tells the server to skip rendering the custom provider component and render it only on the client-side.

To create this component, navigate to the ‘src‘ directory and create a new folder named ‘utils‘. Inside the ‘utils‘ folder, create a file named provider.tsx and add the following code.

src/utils/provider.tsx


"use client";

import React from "react";
import { QueryClientProvider, QueryClient } from "@tanstack/react-query";
import { ReactQueryDevtools } from "@tanstack/react-query-devtools";

function Providers({ children }: React.PropsWithChildren) {
  const [client] = React.useState(
    new QueryClient({ defaultOptions: { queries: { staleTime: 5000 } } })
  );

  return (
    <QueryClientProvider client={client}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

export default Providers;

Create a Request-scoped Instance of QueryClient

To prevent data from being shared across users and requests, while still ensuring that the QueryClient is only created once per request, we can create a request-scoped singleton instance of the QueryClient.

This will make prefetched queries available to all components further down the component tree, and allow us to fetch data within multiple Server Components and use <Hydrate> in multiple places.

You can create a getQueryClient.ts file in the ‘src/utils‘ directory and add the following code snippets in order to achieve this.

src/utils/getQueryClient.ts


import { QueryClient } from "@tanstack/query-core";
import { cache } from "react";

const getQueryClient = cache(() => new QueryClient());
export default getQueryClient;

Create a Custom Hydrate Component

In Next.js 13, there was an issue with prefetching data on the server using the <Hydrate> method, which was raised in the React Query GitHub issues. When we use the <Hydrate> component in a server component, we encounter an error. To work around this, we need to create a custom component that renders the <Hydrate> component on the client-side using the "use client" flag.

To create this custom hydrate component, simply create a file named hydrate.client.tsx in the ‘src/utils‘ directory and add the following code.

src/utils/hydrate.client.tsx


"use client";

import { Hydrate as RQHydrate, HydrateProps } from "@tanstack/react-query";

function Hydrate(props: HydrateProps) {
  return <RQHydrate {...props} />;
}

export default Hydrate;

Provide the QueryClient Provider to Next.js

Next, we’ll wrap the custom provider around the children of the RootLayout component to render the QueryClientProvider at the root. By doing so, all other Client Components across the app will have access to the query client.

As the RootLayout is a Server Component, the custom provider can directly render the QueryClientProvider since it’s marked as a Client Component.

To implement this, simply replace the contents of the layout.tsx file located in the ‘app‘ directory with the following code.

src/app/layout.tsx


import Providers from "@/utils/provider";
import React from "react";
// import "./globals.css";

export const metadata = {
  title: "Create Next App",
  description: "Generated by create next app",
};

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang="en">
      <body>
        <Providers>{children}</Providers>
      </body>
    </html>
  );
}

Please keep in mind that it is best practice to render providers as deep as possible in the component tree. You may have noticed that in our example, the Providers component only wraps around the {children} prop instead of the entire <html> document. This allows Next.js to better optimize the static parts of your Server Components.

Prefetching Data Using Hydration and Dehydration

With React Query set up in our Next.js 13 project, we’re ready to demonstrate how it can be used to fetch data on the server, hydrate the state, dehydrate the cache, and rehydrate it on the client. To illustrate this, we’ll fetch a list of users from the https://jsonplaceholder.typicode.com/ API.

Since we’re using TypeScript, we’ll need to define the structure of the API’s response. To do this, create a types.ts file in the ‘app‘ directory and add the following TypeScript code.

src/app/types.ts


export type User = {
  id: number;
  name: string;
  email: string;
};

Create the Client-Side Component

Now let’s create a Client-side component that displays the list of users fetched from the https://jsonplaceholder.typicode.com/users endpoint.

When this component mounts, it can retrieve the dehydrated query cache if available but will refetch the query on the client if it has become stale since the time it was rendered on the server.

To create this component, navigate to the ‘app‘ directory and create a ‘hydration‘ folder. Inside the ‘hydration‘ folder, create a list-users.tsx file with the following code.

src/app/hydration/list-users.tsx


"use client";

import { User } from "../types";
import { useQuery } from "@tanstack/react-query";
import React from "react";

async function getUsers() {
  const res = await fetch("https://jsonplaceholder.typicode.com/users");
  const users = (await res.json()) as User[];
  return users;
}

export default function ListUsers() {
  const [count, setCount] = React.useState(0);

  const { data } = useQuery({
    queryKey: ["hydrate-users"],
    queryFn: () => getUsers(),
  });

  return (
    <main style={{ maxWidth: 1200, marginInline: "auto", padding: 20 }}>
      <div style={{ marginBottom: "4rem", textAlign: "center" }}>
        <h4 style={{ marginBottom: 16 }}>{count}</h4>
        <button onClick={() => setCount((prev) => prev + 1)}>increment</button>
        <button
          onClick={() => setCount((prev) => prev - 1)}
          style={{ marginInline: 16 }}
        >
          decrement
        </button>
        <button onClick={() => setCount(0)}>reset</button>
      </div>

      {
        <div
          style={{
            display: "grid",
            gridTemplateColumns: "1fr 1fr 1fr 1fr",
            gap: 20,
          }}
        >
          {data?.map((user) => (
            <div
              key={user.id}
              style={{ border: "1px solid #ccc", textAlign: "center" }}
            >
              <img
                src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
                alt={user.name}
                style={{ height: 180, width: 180 }}
              />
              <h3>{user.name}</h3>
            </div>
          ))}
        </div>
      }
    </main>
  );
}

Create the Page Component

Next, we’ll create a Server Component that prefetches the query and passes the prefetched data to the list-users.tsx component.

When the Server Component renders, calls to useQuery nested inside the <Hydrate> Client Component will have access to the prefetched data that is provided in the state property.

To create this Server Component, go to the ‘hydration‘ directory and create a page.tsx file, then add the following code:

src/app/hydration/page.tsx


import getQueryClient from "@/utils/getQueryClient";
import Hydrate from "@/utils/hydrate.client";
import { dehydrate } from "@tanstack/query-core";
import ListUsers from "./list-users";
import { User } from "../types";

async function getUsers() {
  const res = await fetch("https://jsonplaceholder.typicode.com/users");
  const users = (await res.json()) as User[];
  return users;
}

export default async function Hydation() {
  const queryClient = getQueryClient();
  await queryClient.prefetchQuery(["hydrate-users"], getUsers);
  const dehydratedState = dehydrate(queryClient);

  return (
    <Hydrate state={dehydratedState}>
      <ListUsers />
    </Hydrate>
  );
}

With this in place, when you visit http://localhost:3000/hydration and inspect the document in the Network tab of your browser’s development tools, you should see that the data is already available during the initial request, which is beneficial for SEO.

Using React Query in the New Next.js 13 App Directory Workaround Which Involves Using Custom Hydration that is Good for SEO

Prefetching Data Using Initial Data

Let’s explore how to transfer prefetched data from a Server Component, located at a higher level in the component hierarchy, to a Client-side component using props.

However, note that this approach is not recommended by the React Query team, and you should always aim to use the hydration and dehydration method for better performance and reliability.

Create the Client-Side Component

To implement this approach, we’ll create a new Client-side component that will accept the prefetched data as a prop and render it in the UI. Once the data becomes stale, the component will use React Query to refetch the data as usual.

Create a new folder called initial-data inside the ‘app‘ directory. Within this folder, create a file called list-users.tsx, and add the following code:

src/app/initial-data/list-users.tsx


"use client";

import { User } from "../types";
import { useQuery } from "@tanstack/react-query";
import React from "react";

async function getUsers() {
  const res = await fetch("https://jsonplaceholder.typicode.com/users");
  const users = (await res.json()) as User[];
  return users;
}

export default function ListUsers({ users }: { users: User[] }) {
  const [count, setCount] = React.useState(0);

  const { data } = useQuery({
    queryKey: ["initial-users"],
    queryFn: () => getUsers(),
    initialData: users,
  });
  return (
    <main style={{ maxWidth: 1200, marginInline: "auto", padding: 20 }}>
      <div style={{ marginBottom: "4rem", textAlign: "center" }}>
        <h4 style={{ marginBottom: 16 }}>{count}</h4>
        <button onClick={() => setCount((prev) => prev + 1)}>increment</button>
        <button
          onClick={() => setCount((prev) => prev - 1)}
          style={{ marginInline: 16 }}
        >
          decrement
        </button>
        <button onClick={() => setCount(0)}>reset</button>
      </div>

      {
        <div
          style={{
            display: "grid",
            gridTemplateColumns: "1fr 1fr 1fr 1fr",
            gap: 20,
          }}
        >
          {data.map((user) => (
            <div
              key={user.id}
              style={{ border: "1px solid #ccc", textAlign: "center" }}
            >
              <img
                src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
                alt={user.name}
                style={{ height: 180, width: 180 }}
              />
              <h3>{user.name}</h3>
            </div>
          ))}
        </div>
      }
    </main>
  );
}

Create the Page Component

Let’s create a Server Component that prefetches the data and passes it as a prop to the list-users.tsx component.

To do this, create a page.tsx file in the initial-data folder and include the following code:

src/app/initial-data/page.tsx


import ListUsers from "./list-users";
import { User } from "../types";

async function getUsers() {
  const res = await fetch("https://jsonplaceholder.typicode.com/users");
  const users = (await res.json()) as User[];
  return users;
}

export default async function InitialData() {
  const users = await getUsers();

  return <ListUsers users={users} />;
}

When you visit http://localhost:3000/initial-data and inspect the Network tab in your browser’s dev tools, you will notice that the data was initially fetched on the server and then rendered on the client within the cards. This approach is beneficial for SEO since the data is available during server-side rendering and improves the page’s search engine visibility.

Using React Query in the New Next.js 13 App Directory Workaround Which Involves Using Initial Data that is Good for SEO

Conclusion

You can access the React Query and Next.js 13 project’s source code on GitHub.

Throughout this article, you learned how to set up React Query in the Next.js 13 app directory and explored two different ways to prefetch data supported by React Query.

I hope this article was helpful and enjoyable for you. If you have any feedback or questions, please leave a comment below. Thank you for reading!