In this article, you will learn how to use Supabase with React Query in the new Next.js app directory, focusing on both client and server components. Additionally, you will learn how to leverage the Supabase CLI for performing database migrations and seeding data with dummy content for testing.

While Supabase provides a JavaScript SDK for interacting with its hosted PostgreSQL database, you might not necessarily need to use a library like React Query. However, if you aim to incorporate advanced data fetching features, such as re-fetching data in the background when stale or after a mutation, and efficiently caching fetched data, then React Query emerges as the optimal choice for this task.

Without further delay, let’s delve into the article to explore the integration of Supabase with React Query in the new Next.js App Router.

The tutorial’s source code is available on GitHub: https://github.com/wpcodevo/nextjs-react-query-supabase

More practice:

Using React Query with Supabase in Next.js App Router

Bootstrap the Next.js 14+ Project

By the end of this article, you should have a project structure that resembles the one below.

the nextjs 14 supabase and react query project structure

Below is the application we will be building to demonstrate how Supabase and React Query can be used in both client and server components in the new Next.js app router. The app comprises two components, the top one being a form for adding new users and the one below for displaying the users.

nextjs app router project with supabase and react query

Let’s start by initializing a new Next.js 14+ project. To do this, navigate to the folder where you’d like to store the source code and open a new terminal there. Then, execute the following commands based on your preferred package manager.


yarn create nextjs-react-query-supabase
# or 
npx create-next-app@latest nextjs-react-query-supabase
# or
pnpm create next-app nextjs-react-query-supabase

You will encounter prompts to enable specific features for the project. Ensure you choose ‘Yes‘ for TypeScript and ESLint. Regarding the project structure, opt for ‘No‘ for the src/ directory and ‘Yes‘ for the app/ directory. For the import alias, press Enter to accept the default.

After answering all the questions, the Next.js 14+ project will be generated, and the necessary dependencies will be installed. Once the installation is complete, open the project in your preferred IDE or text editor, such as VS Code.

Next, navigate to the app/globals.css file and remove all CSS styles, leaving only the Tailwind CSS directives.

Finally, open the app/page.tsx file and replace its existing content with the one provided below:

app/page.tsx


import Link from 'next/link';

export default function Home() {
  return (
    <div>
      <h1 className='text-4xl font-bold'>Hello, Next.js 14 App Directory!</h1>
      <p>
        <Link className='text-blue-500 underline' prefetch href='/client-side'>
          Using Supabase in a Client Component
        </Link>
      </p>
      <p>
        <Link prefetch href='/server-side' className='text-blue-500 underline'>
          Using Supabase in a Server Component with Hydration
        </Link>
      </p>
    </div>
  );
}

Start the Next.js development server and navigate to the root URL in the browser. You should see a page like this on your screen.

home page of the supabase and react query integration with nextjs app directory

Install the Necessary Dependencies

Now, let’s install the dependencies that will allow us to integrate Supabase and React Query into the Next.js project. Open your terminal and execute the following commands to install these dependencies.


pnpm add @supabase/supabase-js @tanstack/react-query @supabase/ssr
pnpm add -D @tanstack/eslint-plugin-query
pnpm add -D @tanstack/react-query-devtools
pnpm add -D supabase
# or
yarn add @supabase/supabase-js @tanstack/react-query @supabase/ssr
yarn add -D @tanstack/eslint-plugin-query
yarn add -D @tanstack/react-query-devtools
yarn add -D supabase
# or
npm i @supabase/supabase-js @tanstack/react-query @supabase/ssr
npm i -D @tanstack/eslint-plugin-query
npm i -D @tanstack/react-query-devtools
npm i -D supabase

  1. @supabase/supabase-js: The official JavaScript client library for Supabase, offering tools for interacting with Supbase services.
  2. @tanstack/react-query: A data management library for efficiently fetching and managing asynchronous data.
  3. @supabase/ssr: This package facilitates server-side rendering (SSR) support for Supabase in your project.
  4. @tanstack/eslint-plugin-query: A helpful ESLint plugin designed to catch bugs and maintain coding consistency.
  5. @tanstack/react-query-devtools: A development tool for debugging and monitoring React Query within your application.
  6. supabase: Supabase CLI, providing local development tools and deployment capabilities to the Supabase Platform.

Create a Project on Supabase

As usual, we need to create a new Supabase project on the Supabase website. Feel free to use an existing project if you have one. However, if you haven’t set up one yet, follow the steps outlined below.

  1. Visit the Supabase Website:
    Open your web browser and navigate to the Supabase website.
  2. Sign In:
    Sign in using your GitHub account. If you’re new to Supabase, head to the Sign Up page, and click “Continue with GitHub” to create an account.
  3. Create an Organization:
    After signing up, log in to Supabase. You’ll be directed to the dashboard. Click “New Organization” to create one. Provide your organization name (e.g. your company name) and click “Create organization“.
  4. Create a New Project:
    Click “New Project” and select the organization. Provide a project name and generate a secure password. Create a new file in your Next.js project named .env and add the project password as SUPABASE_DATABASE_PASSWORD. Click “Create new project“.
  5. Get the Project URL and Anon Key:
    Copy the project URL and anon key. Add them to the .env file as NEXT_PUBLIC_SUPABASE_URL and NEXT_PUBLIC_SUPABASE_ANON_KEY. If not visible, find them in the settings menu under API settings.
  6. Get the Postgres Connection String:
    On the Settings page, click “Database” and then “URI” under connection string options. Copy the Postgres connection string to the .env file as SUPABASE_DATABASE_URL, replacing [YOUR-PASSWORD] with the project password.
  7. Get the Project Reference:
    Under the Settings menu, click “General” and copy the project reference. Add it to the .env file as SUPABASE_PROJECT_REFERENCE.
  8. Generate an Access Token:
    On the main dashboard, click “Access token” in the left sidebar or use the link https://supabase.com/dashboard/account/tokens. Click “Generate new token“, provide a name, and click “Generate token“. Copy the token to the .env file as SUPABASE_ACCESS_TOKEN.

If you choose to use an existing Supabase project, create a .env file at the root level of your project and include the following environment variables with their respective values.

.env


SUPABASE_ACCESS_TOKEN=
SUPABASE_DATABASE_PASSWORD=
SUPABASE_PROJECT_REFERENCE=
NEXT_PUBLIC_SUPABASE_ANON_KEY=
NEXT_PUBLIC_SUPABASE_URL=
SUPABASE_DATABASE_URL=

Setup Supabase in the Next.js App Router

Now that you have all the necessary environment variables in your .env file, we can proceed to initialize Supabase in the Next.js project. To enable communication between the Supabase CLI and the remote Supabase platform, make sure you have the SUPABASE_ACCESS_TOKEN variable in your .env file.

Initialize and Perform Migrations with the Supabase CLI

To initialize Supabase in the project, execute the command pnpm supabase initYou will be prompted to generate VS Code settings for Deno; press Enter to accept the default. After running the command, you should see a new directory named ‘supabase‘ generated in the root level of your project.


pnpm supabase init
# or
npx supabase init

Next, execute the following command to generate a new migration file in the supabase/migrations directory. If the migrations folder doesn’t already exist, it will be generated along with the migration file.


pnpm supabase migration new init_user
# or
npx supabase migration new init_user

Open the generated migration file and include the following SQL code. This code is designed to create a “User” table in the “public” schema, along with adding a unique constraint on the email to ensure that each user possesses a distinct email address.

supabase/migrations/20240116132802_init_user.sql


-- CreateTable
CREATE TABLE "public"."User" (
    "id" SERIAL NOT NULL,
    "name" TEXT NOT NULL,
    "email" TEXT NOT NULL,
    "role" TEXT NOT NULL DEFAULT 'user',
    "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

    CONSTRAINT "User_pkey" PRIMARY KEY ("id")
);

-- CreateIndex
CREATE UNIQUE INDEX "User_email_key" ON "public"."User"("email");

Also, open the supabase/seed.sql file and add the following SQL code, which will enable us to seed the database with some dummy users for testing.

supabase/seed.sql


INSERT INTO "public"."User" ("name", "email")
VALUES
  ('John Doe', 'john.doe@example.com'),
  ('Jane Smith', 'jane.smith@example.com'),
  ('Alice Johnson', 'alice.johnson@example.com'),
  ('Bob Brown', 'bob.brown@example.com'),
  ('Eva Williams', 'eva.williams@example.com'),
  ('Michael Davis', 'michael.davis@example.com');

With that sorted, it’s time to link our local Supabase project with the remote database. To achieve this, run the command below. Remember to replace the <project_reference> placeholder with the project reference stored in the .env file.


pnpm supabase link --project-ref 
# or
npx supabase link --project-ref 

After running the above command, you will be prompted to input your Supabase project password. Copy the project password from the .env file and paste it into the terminal. Once you are done, press the Enter key, and the project should be linked with the remote database.

At this point, we can apply the migration file to the remote database and seed it with dummy users. Run the following command, replacing the <DB_URL> placeholder with your PostgreSQL connection string.


pnpm supabase db reset --db-url ""
# or
npx supabase db reset --db-url ""

You will be prompted to confirm if you want to reset the remote database. Type ‘Y‘ and press the Enter key.

Generate the Database Types with the Supabase CLI

Now, to generate TypeScript types for our project based on the database schema, create a folder named ‘utils‘ in the root level of your project. Then, run the following command to generate the types and save them in the database.types.ts file within the ‘utils‘ folder.


pnpm supabase gen types typescript --linked --schema=public > utils/database.types.ts
# or
npx supabase gen types typescript --linked --schema=public > utils/database.types.ts

Set Up React Query in the Next.js App Router

In the root level of your project, navigate to the utils directory, and create a new file named queryClient.ts. Add the following code to the file to create a centralized instance of the React Query Client. This instance will be utilized across the application to handle queries and mutations. While you have the flexibility to configure various options, for simplicity, we’ll set a stale time for the cache in this example.

utils/queryClient.ts


import { QueryClient } from '@tanstack/react-query';

const queryClient = new QueryClient({
  defaultOptions: { queries: { staleTime: 5 * 1000 } },
});
export default queryClient;

As of Next.js 14+, all components are server components by default. Wrapping the QueryClientProvider component directly around the {children} node can lead to errors, as it uses hooks available only in the browser. To address this, we must create a client component that renders the QueryClientProvider component. Begin by creating a providers directory at the root of your project. Inside the providers directory, create a ReactQueryClientProvider.tsx file and include the following code:

providers/ReactQueryClientProvider.tsx


'use client';

import queryClient from '@/utils/queryClient';
import { QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

function ReactQueryProvider({ children }: React.PropsWithChildren) {
  return (
    <QueryClientProvider client={queryClient}>
      {children}
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

export default ReactQueryProvider;

In the provided code, we have included the 'use client' pragma at the top of the file. This pragma establishes a boundary between Server and Client Component modules. Consequently, all other modules imported into it, including client components, are recognized as part of the client bundle.

Next, wrap the {children} node with the ReactQueryProvider client component in the app/layout.tsx file. This step ensures that the query client becomes accessible to all components within the component tree.

app/layout.tsx


import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import './globals.css';
import ReactQueryProvider from '@/providers/ReactQueryClientProvider';

const inter = Inter({ subsets: ['latin'] });

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

export default function RootLayout({
  children,
}: {
  children: React.ReactNode;
}) {
  return (
    <html lang='en'>
      <body className={inter.className}>
        <ReactQueryProvider>
          <div>{children}</div>
        </ReactQueryProvider>
      </body>
    </html>
  );
}

It’s essential to render the provider as deeply nested in the tree as possible. In this case, observe how ReactQueryProvider wraps only the {children} element rather than the entire <html> document. This approach aids Next.js in optimizing the static portions of your Server Components more effectively.

Create the Supabase Client

Let’s proceed to create the Supabase browser client, enabling us to perform various operations such as querying the database, inserting, updating, and deleting records, as well as managing authentication.

Essentially, the Supabase client serves as our interface to interact with the various services provided by Supabase. To set this up, create a hooks directory at the root level. Inside the hooks directory, create a useSupabaseClient.ts file and integrate the following code.

hooks/useSupabaseClient.ts


import { useMemo } from 'react';
import { createBrowserClient } from '@supabase/ssr';
import { Database } from '@/utils/database.types';
import { SupabaseClient } from '@supabase/supabase-js';

export type TypedSupabaseClient = SupabaseClient<Database>;
let client: TypedSupabaseClient | undefined;

export function getSupabaseBrowserClient() {
  if (client) {
    return client;
  }

  const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
  const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;

  client = createBrowserClient<Database>(SUPABASE_URL, SUPABASE_ANON_KEY);

  return client;
}

function useSupabaseClient() {
  return useMemo(getSupabaseBrowserClient, []);
}

export default useSupabaseClient;

The useMemo hook ensures that the Supabase client isn’t recreated on every render.

Create a Hook to Fetch Data

Now that we have the Supabase client ready, let’s create a hook responsible for constructing the query to fetch data from the database. To ensure flexibility for usage in both client and server components, we’ll return an object containing the query function, query key, and query options. The ideal approach would involve using the useQuery hook in this file, but it would limit usage to only client components.

Therefore, create a use-users-query.ts file within the hooks directory and incorporate the following code:

hooks/use-users-query.ts


import { QueryKey, UseQueryOptions } from '@tanstack/react-query';
import { TypedSupabaseClient } from './useSupabaseClient';

interface User {
  id: number;
  name: string;
  email: string;
}

export const userQueryKey = ['users'];

function useUsersQuery(
  client: TypedSupabaseClient,
  queryOptions?: UseQueryOptions<User[], Error, User[], QueryKey>
) {
  const queryFn = async (): Promise<User[]> => {
    const users = await client.from('User').select('*').throwOnError();
    return users.data || [];
  };

  return { queryKey: userQueryKey, queryFn, ...queryOptions };
}

export default useUsersQuery;

Create a Hook to Mutate Data

Continuing with our setup, let’s create a hook dedicated to mutating data in the database, specifically for inserting new records. The approach remains similar to the previous one, but this time, we’ll utilize the useMutation hook in this file, as it will exclusively be used in client components. To implement this, create a user-insert-user-mutation.ts file in the hooks directory and include the following code:

hooks/user-insert-user-mutation.ts


import { useMutation } from '@tanstack/react-query';
import useSupabase from './useSupabaseClient';

function useInsertUserMutation() {
  const client = useSupabase();

  const mutationFn = async ({
    name,
    email,
  }: {
    name: string;
    email: string;
  }) => {
    return client
      .from('User')
      .upsert([{ name, email }])
      .throwOnError()
      .select(`*`)
      .throwOnError()
      .single()
      .then((result) => result.data);
  };

  return useMutation({ mutationFn });
}

export default useInsertUserMutation;

Using Supabase and React Query on the Client

Now that we have the Supabase client and query hook ready, we can utilize them to fetch data, specifically the list of users, from the database.

Fetching Data in a Client Component

Start by creating a components directory, and within it, create a list-users.tsx file, and add the following code:

components/list-users.tsx


'use client';

import useUsersQuery from '@/hooks/use-users-query';
import useSupabaseClient from '@/hooks/useSupabaseClient';
import { useQuery } from '@tanstack/react-query';
import Image from 'next/image';

export default function ListUsers() {
  const client = useSupabaseClient();

  const { data } = useQuery(useUsersQuery(client));

  return (
    <>
      {
        <div
          style={{
            display: 'grid',
            gridTemplateColumns: '1fr 1fr 1fr 1fr',
            gap: 20,
          }}
        >
          {data?.map((user) => (
            <div
              key={user.id}
              className='flex flex-col justify-center items-center border-gray-200 border'
            >
              <Image
                src={`https://robohash.org/${user.id}?set=set2&size=180x180`}
                alt={user.name}
                width={180}
                height={180}
                className='block'
              />
              <h3>{user.name}</h3>
            </div>
          ))}
        </div>
      }
    </>
  );
}

In the code above, we utilized the useQuery hook along with the useUsersQuery builder hook and the Supabase client to fetch the list of users from the database. Subsequently, we displayed them in the UI.

You may have noticed that we are displaying images from an external source in the HTML. For this to work, we need to configure the hostname in the next.config.js file. Here is an example:

next.config.js


/** @type {import('next').NextConfig} */
const nextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'robohash.org',
      },
    ],
  },
};

module.exports = nextConfig;

Mutating Data in a Client Component

To insert new users into the database, let’s create a form component for that purpose. This component will utilize the useInsertUserMutation hook we created earlier to mutate data in the database. Create a user-form.tsx file in the components directory and add the following code:

components/user-form.tsx


'use client';

import { userQueryKey } from '@/hooks/use-users-query';
import useInsertUserMutation from '@/hooks/user-insert-user-mutation';
import queryClient from '@/utils/queryClient';
import React, { useState } from 'react';

const UserForm = () => {
  const [name, setName] = useState('');
  const [email, setEmail] = useState('');

  const insertUserMutation = useInsertUserMutation();

  const handleSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
    e.preventDefault();
    insertUserMutation.mutate(
      { name, email },
      {
        onSettled: () => {
          setName('');
          setEmail('');
        },
        onSuccess: async () => {
          await queryClient.invalidateQueries({
            queryKey: userQueryKey,
          });
        },
      }
    );
  };

  return (
    <div className='bg-gray-300 p-8 pt-4 rounded shadow-md max-w-2xl w-full'>
      <h2 className='text-2xl font-semibold mb-4'>Submit Form</h2>

      <form onSubmit={handleSubmit} className='space-y-4'>
        <div className='flex space-x-4 items-end'>
          <div className='w-1/2'>
            <label
              htmlFor='name'
              className='block text-sm font-medium text-gray-700'
            >
              Name
            </label>
            <input
              type='text'
              id='name'
              name='name'
              value={name}
              onChange={(e) => setName(e.target.value)}
              className='mt-1 p-2 w-full border rounded-md'
              required
            />
          </div>

          <div className='w-1/2'>
            <label
              htmlFor='email'
              className='block text-sm font-medium text-gray-700'
            >
              Email
            </label>
            <input
              type='email'
              id='email'
              name='email'
              value={email}
              onChange={(e) => setEmail(e.target.value)}
              className='mt-1 p-2 w-full border rounded-md'
              required
            />
          </div>
          <div className='w-1/4'>
            <button
              type='submit'
              className='w-full block align-bottom bg-blue-500 text-white py-2 rounded-md hover:bg-blue-600'
            >
              Submit
            </button>
          </div>
        </div>
      </form>
    </div>
  );
};

export default UserForm;

Render the Components in a Page File

Now let’s render the UserForm and ListUsers components in a page file. Go into the app directory and create a folder named client-side. Within the client-side directory, create a page.tsx file and add the following code:

app/client-side/page.tsx


import ListUsers from '@/components/list-users';
import UserForm from '@/components/user-form';

export default function Home() {
  return (
    <main style={{ maxWidth: 1200, marginInline: 'auto', padding: 20 }}>
      <div className='w-full flex justify-center mb-8'>
        <UserForm />
      </div>
      <ListUsers />
    </main>
  );
}

With that in place, start the development server again if you haven’t already, and visit the URL http://localhost:3000/client-side. You should see the form component along with the list of users displayed below it. This approach to fetching data is not SEO-friendly because when you inspect the HTML document in the browser, you will only see the form component.

only the submit form is included in the html without the data fetched from supabase

Using Supabase and React Query on the Server

Now, let’s explore how to use Supabase and React Query on the server to prefetch data, ensuring that the data is included in the HTML sent to the browser. This approach is SEO-friendly since the data becomes immediately available to the useQuery hook.

Create the Supabase Server Client

First, let’s create the Supabase server client that will enable us to interact with Supabase on the server. Create a useSupabaseServer.ts file in the hooks directory and include the following code:

hooks/useSupabaseServer.ts


import { cookies } from 'next/headers';
import { createServerClient } from '@supabase/ssr';
import { Database } from '@/utils/database.types';

export function useSupabaseServer(cookieStore: ReturnType<typeof cookies>) {
  const SUPABASE_URL = process.env.NEXT_PUBLIC_SUPABASE_URL!;
  const SUPABASE_ANON_KEY = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;

  return createServerClient<Database>(SUPABASE_URL, SUPABASE_ANON_KEY, {
    cookies: {
      get(name: string) {
        return cookieStore.get(name)?.value;
      },
    },
  });
}

Prefetch Data on the Server

With the Supabase server client ready, let’s use it along with our query client to prefetch data on the server, dehydrate the cache, and hydrate it on the client. To do this, create a server-side folder in the app directory. Within the server-side folder, create a page.tsx file and add the following code:

app/server-side/page.tsx


import useUsersQuery from '@/hooks/use-users-query';
import {
  HydrationBoundary,
  QueryClient,
  dehydrate,
} from '@tanstack/react-query';
import { useSupabaseServer } from '@/hooks/useSupabaseServer';
import { cookies } from 'next/headers';
import ListUsers from '@/components/list-users';
import UserForm from '@/components/user-form';

export default async function Hydation() {
  const queryClient = new QueryClient();
  const cookieStore = cookies();
  const supabase = useSupabaseServer(cookieStore);

  await queryClient.prefetchQuery(useUsersQuery(supabase));

  return (
    <HydrationBoundary state={dehydrate(queryClient)}>
      <main style={{ maxWidth: 1200, marginInline: 'auto', padding: 20 }}>
        <div className='w-full flex justify-center mb-8'>
          <UserForm />
        </div>
        <ListUsers />
      </main>
    </HydrationBoundary>
  );
}

The above code looks similar to the client-side implementation, except that we are prefetching the query on the server and using the HydrationBoundary component to make the query available to the useQuery hook.

When you restart the Next.js development server and visit the URL http://localhost:3000/server-side, you should still see the form component along with the list of users. The only difference is that, when you inspect the HTML document, you will notice that the prefetched data was included in the HTML sent to the browser.

the data was included in the html along with the submit form since the data was prefetched on the server

Conclusion

And that concludes our tutorial. Throughout this guide, you’ve gained insights into integrating Supabase and React Query within the new Next.js app router. I trust you found this article informative and engaging. For the source code, feel free to explore it on GitHub: https://github.com/wpcodevo/nextjs-react-query-supabase