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.

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.

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 need to create a custom provider that will 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, isLoading, isFetching, error } = 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>

      {error ? (
        <p>Oh no, there was an error</p>
      ) : isLoading || isFetching ? (
        <p>Loading...</p>
      ) : data ? (
        <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>
      ) : null}
    </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>
  );
}

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, isLoading, isFetching, error } = 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>

      {error ? (
        <p>Oh no, there was an error</p>
      ) : isLoading || isFetching ? (
        <p>Loading...</p>
      ) : data ? (
        <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>
      ) : null}
    </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} />;
}

Create Links to the Pages

Finally, it’s time to add links to the pages we have created so far on the Home page component. To do this, you can replace the contents of the src/app/page.tsx file with the following code.

src/app/page.tsx


import Link from "next/link";

export default function Home() {
  return (
    <>
      <h1>Hello, Next.js 13 App Directory!</h1>
      <p>
        <Link href="/initial-data">Prefetching Using initial data</Link>
      </p>
      <p>
        <Link href="/hydration">Prefetching Using Hydration</Link>
      </p>
    </>
  );
}

Congratulations! You have successfully set up React Query in a Next.js 13 app and implemented two different data prefetching methods.

To view the app, simply start the Next.js development server and navigate to http://localhost:3000/. The Home page will display two links: Prefetching Using initial data and Prefetching Using Hydration. Clicking on either link will take you to the respective page where you can see the data prefetching method in action.

homepage of the react query next.js 13 app directory demo

Each page contains a counter component that enables you to increase, decrease, and reset the counter. Additionally, the list of users is fetched via React Query and displayed in a grid on the page.

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

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!