💨

Tanstack Query をNext.js(App Router)で導入

2024/06/18に公開

Next.js(App Router)にTanstack Queryを導入する方法について色々と調べたのでメモ

導入のモチベーション

Next.jsではServer ComponentClient Componentの境界が明確で、ユーザー操作が必要なブロックの処理はClient Componentに寄せがちになります。

Tanstack Query を利用すると初期データの読み込みはServer Componentで行い変更後の操作はClient Componentに寄せるなどがちょっとだけやりやすくなります。

Client Componentとして導入

まずは基本的なTanstack Queryの導入方法を使って導入してみましょう。

https://tanstack.com/query/latest/docs/framework/react/overview

App Router向けの導入は後ほど行います。

ベースの設定

まずはClient ComponentとしてProvider を作成します。

app/Provider.tsx
'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>
  )
}

作成したProviderlayout.tsx に導入します。

app/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 clientClient Componentにする必要があるので注意が必要です。

app/page.tsx
'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 に変更することができます。

https://tanstack.com/query/latest/docs/framework/react/guides/advanced-ssr

まずはapp/provider.tsxを以下のように変更します。

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 を以下のように変更します。。

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 clientClient Component にする必要があります。

app/ClientComponent.tsx
"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