Using React Query with Next.js App Router and Supabase Cache Helpers

2024-01-12

11 minute read

TanStack Query, also known as React Query, is an open source state management library for React which handles caching, background updates and stale data out of the box with zero-configuration, which makes it an ideal tool to pair with supabase-js and our auto-generated REST API!

If you prefer video guides, we've got a three-part video series for you!

If you learn better by just jumping into a demo application, you can find one in our examples on GitHub.

Note: this blogpost is inspired by Giancarlo's original blogpost on using React Query with Supabase in Remix.io!

Prerequisites

This article assumes that your have some basic kowledge of building React applications with Next.js. No prior knowledge of React Query or Supabase is required.

We will use the following tools

Install the required dependencies

After you have created your Next.js project, e.g. with npx create-next-app@latest, you can install the required dependencies using the following command:


_10
npm install @supabase/supabase-js @tanstack/react-query @supabase/ssr @supabase-cache-helpers/postgrest-react-query

Creating a React Query client

Create a React Query client in the root of your component tree. In Next.js app router applications, this is the layout.tsx file in the app folder.

The QueryClientProvider can only be used in client components and can't be directly embedded in the layout.tsx file. Therefore make sure to create a client component first, e.g.

components/ReactQueryClientProvider.tsx

_20
'use client'
_20
_20
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
_20
import { useState } from 'react'
_20
_20
export const ReactQueryClientProvider = ({ children }: { children: React.ReactNode }) => {
_20
const [queryClient] = useState(
_20
() =>
_20
new QueryClient({
_20
defaultOptions: {
_20
queries: {
_20
// With SSR, we usually want to set some default staleTime
_20
// above 0 to avoid refetching immediately on the client
_20
staleTime: 60 * 1000,
_20
},
_20
},
_20
})
_20
)
_20
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
_20
}

Next, wrap the root in layout.tsx:

app/layout.tsx

_21
import type { Metadata } from 'next'
_21
import { Inter } from 'next/font/google'
_21
import './globals.css'
_21
import { ReactQueryClientProvider } from '@/components/ReactQueryClientProvider'
_21
_21
const inter = Inter({ subsets: ['latin'] })
_21
_21
export const metadata: Metadata = {
_21
title: 'Create Next App',
_21
description: 'Generated by create next app',
_21
}
_21
_21
export default function RootLayout({ children }: { children: React.ReactNode }) {
_21
return (
_21
<ReactQueryClientProvider>
_21
<html lang="en">
_21
<body className={inter.className}>{children}</body>
_21
</html>
_21
</ReactQueryClientProvider>
_21
)
_21
}

Creating your Database schema and generating TypeScript types

For this example, we'll use a simple countries table where we store the id and name of countries. In your Supabase Dashboard SQL editor create the countries table and add some values:


_11
create table countries (
_11
"id" serial primary key,
_11
"name" text
_11
);
_11
_11
insert into countries
_11
(id, name)
_11
values
_11
(1, 'United Kingdom'),
_11
(2, 'United States'),
_11
(3, 'Singapore');

Once you've created your schema, you can use the Supabase CLI to automatically generate TypeScript types for you:


_10
supabase login
_10
supabase init
_10
supabase link
_10
supabase gen types typescript --linked --schema=public > utils/database.types.ts

These generated types will allow us to get typed data returned from React Query.

Creating supabase-js clients for client and server components

To help you utilize the full power of supabase-js, including Supabase Auth and Row Level Security (RLS) policies, we provide the Supabase SSR helper library that allows you to conveniently create both browser Supabase clients for client components and server Supabase clients for server components.

Further reading: detailed documentation for Supabase SSR in Next.js

Create a TypedSupabaseClient type

To make sure we have the proper typing available in all our components, we can create a TypedSupabaseClient type that we can hand to React Query:

utils/types.ts

_10
import { SupabaseClient } from '@supabase/supabase-js'
_10
import type { Database } from '@/utils/database.types'
_10
_10
export type TypedSupabaseClient = SupabaseClient<Database>

Creating a Browser Supabase Client

utils/supabase-browser.ts

_25
import { createBrowserClient } from '@supabase/ssr'
_25
import type { Database } from '@/utils/database.types'
_25
import type { TypedSupabaseClient } from '@/utils/types'
_25
import { useMemo } from 'react'
_25
_25
let client: TypedSupabaseClient | undefined
_25
_25
function getSupabaseBrowserClient() {
_25
if (client) {
_25
return client
_25
}
_25
_25
client = createBrowserClient<Database>(
_25
process.env.NEXT_PUBLIC_SUPABASE_URL!,
_25
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
_25
)
_25
_25
return client
_25
}
_25
_25
function useSupabaseBrowser() {
_25
return useMemo(getSupabaseBrowserClient, [])
_25
}
_25
_25
export default useSupabaseBrowser

Creating a Server Supabase Client

utils/supabase-server.ts

_17
import { createServerClient } from '@supabase/ssr'
_17
import { cookies } from 'next/headers'
_17
import { Database } from './database.types'
_17
_17
export default function useSupabaseServer(cookieStore: ReturnType<typeof cookies>) {
_17
return createServerClient<Database>(
_17
process.env.NEXT_PUBLIC_SUPABASE_URL!,
_17
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
_17
{
_17
cookies: {
_17
get(name: string) {
_17
return cookieStore.get(name)?.value
_17
},
_17
},
_17
}
_17
)
_17
}

Now we've got everything in place to get started fetching and caching data with React Query!

Automate query key management with the Supabase Cache Helpers

React Query manages query caching based on query keys. Needing to manage query keys is somewhat burdensome, luckily this is where the Supabase Cache Helpers come into play.

Initially built during the Launch Week 5 Hackathon by Philipp Steinrötter, it has become a full blown open source project that automatically generates cache keys from your supabase-js queries, amongst many other awesome features!

Write reusable queries

The most convenient way to use your queries across both server and client component is to define them in a central place, e.g. a queries folder:

queries/get-country-by-id.ts

_15
import { TypedSupabaseClient } from '@/utils/types'
_15
_15
export function getCountryById(client: TypedSupabaseClient, countryId: number) {
_15
return client
_15
.from('countries')
_15
.select(
_15
`
_15
id,
_15
name
_15
`
_15
)
_15
.eq('id', countryId)
_15
.throwOnError()
_15
.single()
_15
}

This is a simple query function that takes in either the browser or the server Supabase client and the id of a country, and returns a supabase-js query.

Fetch data server side

In server components, we can now use this query with the prefetchQuery method:

app/ssrcountries/[id]/page.tsx

_22
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
_22
import { prefetchQuery } from '@supabase-cache-helpers/postgrest-react-query'
_22
import useSupabaseServer from '@/utils/supabase-server'
_22
import { cookies } from 'next/headers'
_22
import Country from '../country'
_22
import { getCountryById } from '@/queries/get-country-by-id'
_22
_22
export default async function CountryPage({ params }: { params: { id: number } }) {
_22
const queryClient = new QueryClient()
_22
const cookieStore = cookies()
_22
const supabase = useSupabaseServer(cookieStore)
_22
_22
await prefetchQuery(queryClient, getCountryById(supabase, params.id))
_22
_22
return (
_22
// Neat! Serialization is now as easy as passing props.
_22
// HydrationBoundary is a Client Component, so hydration will happen there.
_22
<HydrationBoundary state={dehydrate(queryClient)}>
_22
<Country id={params.id} />
_22
</HydrationBoundary>
_22
)
_22
}

Our query will be executed and fetch the data on the server. This means when using our query in the corresponding Country client component, the data will be immediately available upon render:

app/ssrcountries/country.tsx

_18
'use client'
_18
_18
import useSupabaseBrowser from '@/utils/supabase-browser'
_18
import { getCountryById } from '@/queries/get-country-by-id'
_18
import { useQuery } from '@supabase-cache-helpers/postgrest-react-query'
_18
_18
export default function Country({ id }: { id: number }) {
_18
const supabase = useSupabaseBrowser()
_18
// This useQuery could just as well happen in some deeper
_18
// child to <Posts>, data will be available immediately either way
_18
const { data: country } = useQuery(getCountryById(supabase, id))
_18
_18
return (
_18
<div>
_18
<h1>SSR: {country?.name}</h1>
_18
</div>
_18
)
_18
}

Since our query has them same generated cache key, React Query knows that the data was pre-fetched server side and therefore can render immediately without any loading state.

Fetch data client side

Of course you can still combine this with fetching data client side. React Query will check if a given query was pre-fetched server side, but if it wasn't it will then go ahead and fetch the data client side side using the browser Supabase client:

app/countries/[id]/page.tsx

_24
'use client'
_24
_24
import useSupabaseBrowser from '@/utils/supabase-browser'
_24
import { getCountryById } from '@/queries/get-country-by-id'
_24
import { useQuery } from '@supabase-cache-helpers/postgrest-react-query'
_24
_24
export default function CountryPage({ params }: { params: { id: number } }) {
_24
const supabase = useSupabaseBrowser()
_24
const { data: country, isLoading, isError } = useQuery(getCountryById(supabase, params.id))
_24
_24
if (isLoading) {
_24
return <div>Loading...</div>
_24
}
_24
_24
if (isError || !country) {
_24
return <div>Error</div>
_24
}
_24
_24
return (
_24
<div>
_24
<h1>{country.name}</h1>
_24
</div>
_24
)
_24
}

Conclusion

React Query and the Supabase Cache Helpers are fantastic tools to help you manage data fetching and caching in your Next.js applications.

Using React Query with Server Components makes most sense if:

  • You have an app using React Query and want to migrate to Server Components without rewriting all the data fetching.
  • You want a familiar programming paradigm, but want to still sprinkle in the benefits of Server Components where it makes most sense.
  • You have some use case that React Query covers, but that your framework of choice does not cover.

It's hard to give general advice on when it makes sense to pair React Query with Server Components and not. If you are just starting out with a new Server Components app, we suggest you start out with any tools for data fetching your framework provides you with and avoid bringing in React Query until you actually need it. This might be never, and that's fine, as always: use the right tool for the job!

More Next.js and Supabase resources

Share this article

Build in a weekend, scale to millions