Tanstack Query をNext.js(App Router)で導入
Next.js(App Router)にTanstack Queryを導入する方法について色々と調べたのでメモ
導入のモチベーション
Next.jsではServer ComponentとClient Componentの境界が明確で、ユーザー操作が必要なブロックの処理はClient Componentに寄せがちになります。
Tanstack Query を利用すると初期データの読み込みはServer Componentで行い変更後の操作はClient Componentに寄せるなどがちょっとだけやりやすくなります。
Client Componentとして導入
まずは基本的なTanstack Queryの導入方法を使って導入してみましょう。
App Router向けの導入は後ほど行います。
ベースの設定
まずはClient ComponentとしてProvider を作成します。
'use client'
import { QueryClient, QueryClientProvider } from '@tanstack/react-query'
export default function Provider(
{ children } : { children: React.ReactNode }
) {
const queryClient = new QueryClient()
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
作成したProviderをlayout.tsx に導入します。
import Provider from './provider'
export default function RootLayout(
{ children } : { children: React.ReactNode }
) {
return (
<html lang="ja">
<body>
<Provider>
{children}
</Provider>
</body>
</html>
);
}
利用方法
読み込みページでは以下のようにuseQueryを利用してデータの読み込みを行います。
利用時はuseQueryというカスタムフックを利用するのでuse client でClient Componentにする必要があるので注意が必要です。
'use client'
import { useQuery } from '@tanstack/react-query'
export default function Page() {
const { isPending, error, data } = useQuery({
queryKey: ['fetchDate'],
queryFn: () =>
fetch('http://localhost:3000/lazy-api?time=100').then((res) =>
res.json(),
),
})
if (isPending) return 'Loading...'
if (error) return 'An error has occurred: ' + error.message
return (
<div>
<h1>{data.date}</h1>
</div>
)
}
この場合はAPI通信はCSR 時に行われていてSSRで配信されるHTMLには「Loading...」と記述されています。
データの取得をServer Component に移動
App Routerを利用するのであればデータの取得をSSR時のみに行うServer Component に変更することができます。
まずはapp/provider.tsxを以下のように変更します。
'use client'
import {
isServer,
QueryClient,
QueryClientProvider,
} from '@tanstack/react-query'
function makeQueryClient() {
return new QueryClient({
defaultOptions: {
queries: {
staleTime: 60 * 1000,
},
},
})
}
let browserQueryClient: QueryClient | undefined = undefined
function getQueryClient() {
if (isServer) {
return makeQueryClient()
} else {
if (!browserQueryClient) browserQueryClient = makeQueryClient()
return browserQueryClient
}
}
export default function Provider(
{ children } : { children: React.ReactNode }
) {
const queryClient = getQueryClient()
return (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
)
}
makeQueryClient()内のstaleTime: 60 * 1000がポイントでこちらの指定がないとSSR時とCSR時と2回通信が発生してしまいます。
SSR時に通信を行うapp/page.tsx を以下のように変更します。。
import { dehydrate, HydrationBoundary } from '@tanstack/react-query'
import { ClientComponent } from "./ClientComponent";
export async function fetchDate() {
const res = await fetch(
'http://localhost:3000/lazy-api'
)
return await res.json()
}
export default async function Page() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery({
queryKey: ['fetchDate'],
queryFn: fetchDate,
})
const dehydratedState = dehydrate(queryClient)
return (
<main>
<HydrationBoundary state={dehydratedState}>
<ClientComponent />
</HydrationBoundary>
</main>
);
}
fetchDate() とqueryFn: fetchDate がポイントでこれでSSR時に通信を行い通信結果をキャッシュしています。
データを表示するClientComponentはカスタムフック(useQuery)を利用するのでuse client でClient Component にする必要があります。
"use client"
import { useQueryClient } from '@tanstack/react-query'
export function ClientComponent(){
const queryClient = useQueryClient()
const data:any = queryClient.getQueryData(['fetchDate'])
return (
<div>
<h1>{data.date}</h1>
</div>
)
}
データの取得はpage.tsxのSSR時に行われているので取得するHTML上には<h1>2024-06-14T18:23:13.798Z</h1> と表示されて、queryClient.getQueryData() でprefetchしたキャッシュデータを取得しているのでCSR 時に通信は発生しないです。
Discussion