🐶

[Next.js] Tanstack Query の SSR での利用法

2023/10/01に公開

概要

今回は実務の中で、Tanstack Query を用いた SSR の実装を行ったので、その使用方法を記事としてまとめていこうと思います。

基本的には公式のドキュメントを参照しながらまとめたため、大きな解釈違いはないと思っているのですが、間違っている箇所などあれば気軽にご指摘いただけると嬉しいです。

https://tanstack.com/query/latest

// 使用技術
- Next.js (pages router)
- Tanstack Query

Tanstack Query(React Query)とは

まず、そもそも Tanstack Query とはどのようなライブラリなのかを説明していきたいと思います。
公式には、「Tanstack Query はよくデータを取得するライブラリと説明されるが、より技術的に言えば、サーバーの状態の取得、キャッシュ、同期、更新を簡単に行うことができる」と説明がされています。

ほとんどのWebフレームワークには、データの取得または更新するための独自の方法が付属しているわけではないので、サーバーを介したデータのやり取りを行う中で以下のようなものが問題になります。

  • キャッシュの管理
  • 同じAPIに対しての複数のリクエスト
  • データが更新されたの認知ができない
  • ページネーションやデータの遅延読み込みなどのパフォーマンスの最適化
    などなど...

そのため、ここ挙げた問題を解決するための方法として、Tanstack Query の導入を検討することになります。基本的な実装は公式に則りますが、以下のような形で実装を行います。

import {
  QueryClient,
  QueryClientProvider,
  useQuery,
} from '@tanstack/react-query'

const queryClient = new QueryClient()

export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <Example />
    </QueryClientProvider>
  )
}

function Example() {
  const { isLoading, error, data } = useQuery({
    queryKey: ['repoData'],
    queryFn: () =>
      fetch('https://api.github.com/repos/TanStack/query').then(
        (res) => res.json(),
      ),
  })

  if (isLoading) return 'Loading...'

  if (error) return 'An error has occurred: ' + error.message

  return (
    <div>
      <h1>{data.name}</h1>
      <p>{data.description}</p>
      <strong>👀 {data.subscribers_count}</strong>{' '}
      <strong>{data.stargazers_count}</strong>{' '}
      <strong>🍴 {data.forks_count}</strong>
    </div>
  )
}

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

Tanstack Query を用いたSSRの実装方法

少し Tanstack Query の基本的な実装の説明が長くなりましたが、本題の Tanstack Query を用いた SSR での実装方法について説明していきます。
Tanstack Query は SSR でのデータの取得方法として、2つの方法を提供しています。

1. InitialData で渡す方法

1つ目の方法は、SSR 時に取得したデータを useQuery の initialData として、クライアント側に流してあげる方法になります。

まず、SSR で行う処理に関しては、Next.js で紹介されているような一般的な実装を行います。

// page/index.tsx
import type { GetServerSideProps } from 'next';

export const getServerSideProps: GetServerSideProps = async () => {
  const getData = await fetch('https://pokeapi.co/api/v2/pokemon/ditto')
    .then((res) => res.json());

  return {
    props: { getData },
  }
};

そして、その SSR で行ったデータの受け取り方もシンプルで、データを Props で受け取り、useQuery の initialData に対して、そのデータを渡してあげることで完了です。

// page/index.tsx
import { useQuery } from '@tanstack/react-query';
import Image from 'next/image';

const QuerySSR = (props: Props) => {
  const {
    getData,
  } = props;

  const { isLoading, error, data } = useQuery({
    queryKey: ['poke'],
    queryFn: (() => {
      return fetch('https://pokeapi.co/api/v2/pokemon/ditto').then((res) => res.json());
    }),
    initialData: getData,
  });

  if (isLoading) return 'Loading...'

  if (error) return 'An error has occurred: ' + error

  return (
    <main style={ { margin: '50px' } }>
      <h3>Hello! { data.forms[0].name }!</h3>
      <Image
        src={ data.sprites.front_default }
        width={ 300 }
        height={ 300 }
        alt=''
      />
    </main>
  );
};

このように記事にするまでもないくらいあっさりと実装することができます。ですが、この方法ではいくつかの問題点があることが公式に載せられています。

  1. useQuery をコンポーネントの深い場所で呼び出している場合、initialData をそのコンポーネントまでバケツリレーで流していく必要があります。
  2. 複数の場所で同じ API の呼び出しを行なっている場合、すべての場所で同じように initialData を渡さなければなりません。

そのため、セットアップの実装としては簡単なのですが、完璧とは言えないものとなっています。

2. Hydration を使用する方法

2つ目の方法は、Tanstack Query が提供している queryClient を使用し、SSR で取得したデータをハイドレートする方法です。(ハイドレートの説明はこちら

まず Hydration を使用する方法では、_app.tsx に変更を加えます。

// _app.tsx
import {
  Hydrate,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'
import type { DehydratedState } from '@tanstack/react-query';

type Props = {
  dehydratedState: DehydratedState;
};

type AppPropsWithLayout = AppProps<Props> & {
  Component: NextPageWithLayout<Props>;
};

export default function MyApp(props: AppPropsWithLayout) {
  const { Component, pageProps } = props;
  const [queryClient] = React.useState(() => new QueryClient())

  return (
    <QueryClientProvider client={queryClient}>
      <Hydrate state={pageProps.dehydratedState}>
        <Component {...pageProps} />
      </Hydrate>
    </QueryClientProvider>
  )
}

最初に紹介した InitialData での方法でも同じように QueryClientProvider でコンポーネントをラップする実装を行います。

しかし、先述した実装と異なる点が2つあります。
QueryClient の作成を useState に格納することと、Hydrateコンポーネントに対してpageProps.dehydratedState を渡してあげることです。

QueryClient の作成を useState に格納することの背景については、公式にも詳しい記載がありませんでした。ですが、サーバーとクライアントで QueryClient を共有する必要があるため、useState を使用して、データの一貫性を持たせるという背景があるのかなと思います。

Hydrateコンポーネントに関しては、後に登場する dehydrate を使用する際などに使用するコンポーネントのために必要なものとなっています。

次に、SSR とページコンポーネントの実装を見てみましょう。

import { dehydrate, QueryClient, useQuery } from '@tanstack/react-query'
import Head from 'next/head';
import Image from 'next/image';

import type { GetServerSideProps } from 'next';

const QuerySSR = () => {
  const { isLoading, error, data } = useQuery({
    queryKey: ['poke'],
    queryFn: (() => {
      return fetch('https://pokeapi.co/api/v2/pokemon/ditto').then((res) => res.json());
    }),
  });

  if (isLoading) return 'Loading...'

  if (error) return 'An error has occurred: ' + error

  return (
    <main style={ { margin: '50px' } }>
      <Head>
        <title>Pokemon</title>
      </Head>

      <h3>Hello! { data.forms[0].name }!</h3>
      <Image
        src={ data.sprites.front_default }
        width={ 300 }
        height={ 300 }
        alt=''
      />
    </main>
  );
};

export const getServerSideProps: GetServerSideProps = async () => {
  const queryClient = new QueryClient();

  await queryClient.prefetchQuery(['poke'], () => {
    return fetch('https://pokeapi.co/api/v2/pokemon/ditto').then((res) => res.json())
  });

  return {
    props: {
      dehydratedState: dehydrate(queryClient),
    }
  }
};

このように、SSR でデータを props に渡すタイミングで、dehydrate という Tanstack Query のキャッシュに値を追加する処理を使用します。

この方法は実装は少し複雑になりますが、InitialData のような問題は提示されていないため、こちらの実装方法を使用するのが良いのかなと思います。

https://tanstack.com/query/v4/docs/react/guides/ssr

まとめ

この記事では、Tanstack Query を SSR で使用する方法について紹介しました。何となくで使用していた部分も多かったため、実際に調査しながら書き進めてすごく勉強になったなぁという気持ちです。一方で、個人的な解釈で書き進めてしまっている部分もあったりするので、間違っている箇所などがあれば、優しく指摘していただけると嬉しく思います。

最後に、今まで書いてきたコードをブラウザで表示すると、メタモンが表示されます。こういう試しで API を叩く際に、ポケモンの API が楽に可愛く使えて嬉しいですね。

メタモン

Discussion