apps/www/_blog/2024-01-12-react-query-nextjs-app-router-cache-helpers.mdx
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!
<div className="video-container"> <iframe className="w-full" src="https://www.youtube-nocookie.com/embed/videoseries?si=FRqjgxBa4xmLU1Y0&list=PL5S4mPUpp4OuCRhBFPvMTTWFHNrmAOCD_" title="YouTube video player" allow="accelerometer; autoplay; clipboard-write; encrypted-media; fullscreen; gyroscope; picture-in-picture; web-share" allowfullscreen /> </div>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!
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
14.0.35.12.20.0.10@supabase-cache-helpers/postgrest-react-query version 1.3.0After 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:
npm install @supabase/supabase-js @tanstack/react-query @supabase/ssr @supabase-cache-helpers/postgrest-react-query
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.
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
import { useState } from 'react'
export const ReactQueryClientProvider = ({ children }: { children: React.ReactNode }) => {
const [queryClient] = useState(
() =>
new QueryClient({
defaultOptions: {
queries: {
// With SSR, we usually want to set some default staleTime
// above 0 to avoid refetching immediately on the client
staleTime: 60 * 1000,
},
},
})
)
return <QueryClientProvider client={queryClient}>{children}</QueryClientProvider>
}
Next, wrap the root in layout.tsx:
import type { Metadata } from 'next'
import { Inter } from 'next/font/google'
import './globals.css'
import { ReactQueryClientProvider } from '@/components/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 (
<ReactQueryClientProvider>
<html lang="en">
<body className={inter.className}>{children}</body>
</html>
</ReactQueryClientProvider>
)
}
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:
create table countries (
"id" serial primary key,
"name" text
);
insert into countries
(id, name)
values
(1, 'United Kingdom'),
(2, 'United States'),
(3, 'Singapore');
Once you've created your schema, you can use the Supabase CLI to automatically generate TypeScript types for you:
supabase login
supabase init
supabase link
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.
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
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:
import { SupabaseClient } from '@supabase/supabase-js'
import type { Database } from '@/utils/database.types'
export type TypedSupabaseClient = SupabaseClient<Database>
import { createBrowserClient } from '@supabase/ssr'
import type { Database } from '@/utils/database.types'
import type { TypedSupabaseClient } from '@/utils/types'
import { useMemo } from 'react'
let client: TypedSupabaseClient | undefined
function getSupabaseBrowserClient() {
if (client) {
return client
}
client = createBrowserClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!
)
return client
}
function useSupabaseBrowser() {
return useMemo(getSupabaseBrowserClient, [])
}
export default useSupabaseBrowser
import { createServerClient } from '@supabase/ssr'
import { cookies } from 'next/headers'
import { Database } from './database.types'
export default function useSupabaseServer(cookieStore: ReturnType<typeof cookies>) {
return createServerClient<Database>(
process.env.NEXT_PUBLIC_SUPABASE_URL!,
process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!,
{
cookies: {
get(name: string) {
return cookieStore.get(name)?.value
},
},
}
)
}
Now we've got everything in place to get started fetching and caching data with React Query!
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!
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:
import { TypedSupabaseClient } from '@/utils/types'
export function getCountryById(client: TypedSupabaseClient, countryId: number) {
return client
.from('countries')
.select(
`
id,
name
`
)
.eq('id', countryId)
.throwOnError()
.single()
}
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.
In server components, we can now use this query with the prefetchQuery method:
import { dehydrate, HydrationBoundary, QueryClient } from '@tanstack/react-query'
import { prefetchQuery } from '@supabase-cache-helpers/postgrest-react-query'
import useSupabaseServer from '@/utils/supabase-server'
import { cookies } from 'next/headers'
import Country from '../country'
import { getCountryById } from '@/queries/get-country-by-id'
export default async function CountryPage({ params }: { params: { id: number } }) {
const queryClient = new QueryClient()
const cookieStore = cookies()
const supabase = useSupabaseServer(cookieStore)
await prefetchQuery(queryClient, getCountryById(supabase, params.id))
return (
// Neat! Serialization is now as easy as passing props.
// HydrationBoundary is a Client Component, so hydration will happen there.
<HydrationBoundary state={dehydrate(queryClient)}>
<Country id={params.id} />
</HydrationBoundary>
)
}
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:
'use client'
import useSupabaseBrowser from '@/utils/supabase-browser'
import { getCountryById } from '@/queries/get-country-by-id'
import { useQuery } from '@supabase-cache-helpers/postgrest-react-query'
export default function Country({ id }: { id: number }) {
const supabase = useSupabaseBrowser()
// This useQuery could just as well happen in some deeper
// child to <Posts>, data will be available immediately either way
const { data: country } = useQuery(getCountryById(supabase, id))
return (
<div>
<h1>SSR: {country?.name}</h1>
</div>
)
}
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.
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:
'use client'
import useSupabaseBrowser from '@/utils/supabase-browser'
import { getCountryById } from '@/queries/get-country-by-id'
import { useQuery } from '@supabase-cache-helpers/postgrest-react-query'
export default function CountryPage({ params }: { params: { id: number } }) {
const supabase = useSupabaseBrowser()
const { data: country, isLoading, isError } = useQuery(getCountryById(supabase, params.id))
if (isLoading) {
return <div>Loading...</div>
}
if (isError || !country) {
return <div>Error</div>
}
return (
<div>
<h1>{country.name}</h1>
</div>
)
}
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:
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!