SSR(Server Side Rendering)で、TanStack Query(旧:ReactQuery)を使ってデータをフェッチする

皆さん、こんにちは。技術開発グループのn-ozawanです。
オーストラリア旅行、楽しかったです。コアラ抱っこしました。コアラに過度なストレスを与えないよう、「自分はユーカリの木!」と自己暗示をかけながら抱っこしました。

本題です。
取得したデータを適切にキャッシュしてくれるツールとしてTanStack Queryが人気です。クライアントでデータをキャッシュするのであれば何も考慮はしなくても良いですが、サーバー側でキャッシュする場合は一工夫が必要になります。今回はサーバーサイドでキャッシュしたデータをクライアントと共有する方法についてお話しします。

TanStack Query

概要

TanStack Query (旧:React Query)は、データの取得とそのキャッシュを提供するツールです。公式サイトでは「強力な非同期状態管理ツール」であると紹介されており、データ取得とキャッシュに留まらない強力なツールでもあります。

主にAPI呼び出しとセットで使われます。例えば、フロントエンドからバックエンドへAPI送信する際、その応答が返ってくるまでタイムラグが発生します。そのタイムラグはUXに直結する問題で、応答が返ってくる間、画面に何も動きが無いと人は不安になるものです。TanStack Queryは、読み込み中、応答あり、などの状態を効率的に管理してくれるツールです。

以下は公式サイトに掲載されているソースコードです。

function Todos() {
  const { isPending, isError, data, error } = useQuery({
    queryKey: ['todos'],
    queryFn: fetchTodoList,
  })

  if (isPending) {
    return <span>Loading...</span>
  }

  if (isError) {
    return <span>Error: {error.message}</span>
  }

  // We can assume by this point that `isSuccess === true`
  return (
    <ul>
      {data.map((todo) => (
        <li key={todo.id}>{todo.title}</li>
      ))}
    </ul>
  )
}

冒頭のuserQuery(...)関数がTanStackの関数になります。userQuery(...)関数のqueryFn引数にAPI送信処理を記述します。

isPendingtrueの場合はデータの読み込み中になります。isErrortrueの場合はデータの読み込みに失敗したことになります。それぞれで適切に表示内容を切り分けています。isPendingisErrorがともにfalseとなった場合(つまり、データ取得に成功した場合)、取得したデータを表示しています。

他にも、キャッシュしてから時間が経った、古いキャッシュデータを再読み込みで最新化したりなど、キャッシュデータの状態管理もしてくれます。

SSRでのプリフェッチ

SSR (Server Side Rendering) にて、事前にデータを取得しキャッシュに保存することで、画面表示が早くなります。以下は公式サイトに掲載されているソースコードです。

// For Remix, rename this to loader instead
export async function getServerSideProps() {
  const queryClient = new QueryClient()

  const user = await queryClient.fetchQuery({
    queryKey: ['user', email],
    queryFn: getUserByEmail,
  })

  if (user?.userId) {
    await queryClient.prefetchQuery({
      queryKey: ['projects', userId],
      queryFn: getProjectsByUser,
    })
  }

  // For Remix:
  // return json({ dehydratedState: dehydrate(queryClient) })
  return { props: { dehydratedState: dehydrate(queryClient) } }
}

SSR冒頭にconst queryClient = new QueryClient()によりQueryClientを生成します。queryClient.fetchQuery(...)関数によりデータの取得とキャッシュ保存を行います。上記のソースコードでは2回呼び出しています。最後にdehydrate(queryClient)を返却します。

SSRでのデータ取得は以上ですが、これだけでは取得したデータを画面に表示することは出来ません。Hydrationと呼ばれる処理が必要になります。

Hydration

Hydrationについて説明する前に、そもそもSSRとは何かを説明します。

SSRを使わず画面を表示する場合、サーバーはクライアントへ必要最小限のHTMLを送信します。クライアントは受け取った必要最小限のHTMLから、JavaScriptを処理して表示する画面を構築し、ブラウザに表示します。これをCSR (Client Side Rendering)と言います。

このCSRは描画処理を各クライアントで行うため、描画処理が遅くなる、というデメリットがあります。そこで考えられたのがSSR (Server Side Rendering) です。SSRでは、サーバー側でJavaScriptを処理して画面を構築し、構築した静的HTMLをクライアントへ送信します。クライアントは静的HTMLを画面に表示するだけですので、画面の描画処理は早くなります。

しかし、このSSRは静的HTMLを送信するため、動的に画面を動かすような処理が行えません。この問題を解消するために、静的HTMLとは別に、動的に動かすためのJavaScriptを別途クライアントへ送信します。この処理をHydrationと呼びます。

TanStack Queryにおいても、サーバー側にキャッシュしたデータをクライアントと共有するため、Hydrationを行う必要があります。以下は公式サイトに掲載されているソースコードです。

// _app.tsx
import {
  HydrationBoundary,
  QueryClient,
  QueryClientProvider,
} from '@tanstack/react-query'

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

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

// pages/posts.tsx
// Remove PostsRoute with the HydrationBoundary and instead export Posts directly:
export default function Posts() { ... }

上記で注目すべきところはHydrationBoundary要素です。state属性にpageProps.dehydratedStateを渡していますが、これは「SSRでのプリフェッチ」で紹介したソースコードで、最後に返却していたdehydrate(queryClient)です。このHydrationBoundary要素によりHydrationが行われ、サーバーとクライアントでキャッシュを共有することが出来るようになります。

おわりに

まだ私はTanStack QueryのuseQueryuseMutationしか使ったことないのですが、他にもuseSuspenseQueryuseInfiniteQueryなど、気になる機能がありますね。機会があれば使ってみたいです。

ではまた。

Recommendおすすめブログ