aptpod Tech Blog

株式会社アプトポッドのテクノロジーブログです

Next.js とReact Query でデータを表示したり更新する

こんにちは。intdash グループ フロントエンドエンジニアの佐藤です。

Next.js を使った管理画面を作成するプロジェクトを担当する機会がありました。 管理画面は「頻繁にデータが更新されることがない」、「同時アクセスはあまり起きない」という前提の元に作成することが多いと考えています。 なのでサーバー、表示ともにパフォーマンス的に無駄なリクエストを投げたくはありません。

それを解決するため、プロジェクトにReact Query を導入しました。 今回はその導入事例をご紹介したいと思います。

SSRを使用してデータを表示する

SSR については下記のリンクが参考になります。

nextjs.org

まずサーバーサイドはgetServerSideProps でデータを取得します。

// index.tsx

const USER_KEY = "USER_KEY";

const fetchUser = async (baseUrl: string): Promise<{ name: string }> => {
  const res = await fetch(`${baseUrl}/api/user`);

  return await res.json();
};

export const getServerSideProps: GetServerSideProps<ServerSideProps> = async ({
  req,
}) => {
  const queryClient = new QueryClient();

  const protocol = req.headers["x-forwarded-proto"] || "http";
  const baseUrl = req ? `${protocol}://${req.headers.host}` : "";

  await queryClient.prefetchQuery([USER_KEY], () => fetchUser(baseUrl));

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

次にfetchUser でPrefetchしたデータをUSER_KEY のQuery に紐づけます。 dehydrate でクエリキャッシュをDehydrate し、dehydratedState を介してページにProps として渡します。

queryClient.prefetchQuery で特徴的なのはその返り値です。 公式ドキュメントにある通りPromise<void> を返します。エラーもスローされません。 エラーのハンドリングや取得したデータをサーバーサイドで使用する場合はqueryClient.fetchQuery を使います。

(参考: https://github.com/TanStack/query/discussions/1494#discussioncomment-226271 )

クライアントサイドではuseQuery を使って実装します。

const Page: React.FC<ServerSideProps> = () => {
  const { data } = useQuery([USER_KEY], () => fetchUser(), {
    staleTime: Infinity,
  });


  return (
    <div>
      <p>{data.name}</p>
    </div>
  );
};

useQuery のQuery Key はサーバーサイドと同様にUSER_KEY を指定します。 オプションにはstaleTime: Infinity を追加しました。 このオプションによりキャッシュは常に「新鮮 (新しい)」と見なされ、バックグラウンドでの再取得が発生しません。

ブラウザでのReact Query の動作の様子

StaleTime やCacheTime については下記のブログがわかりやすかったです。

tkdodo.eu

データを更新する

「データを表示する」でオプションにstaleTime: Infinity を追加しました。 これによりデータの再取得は「キャッシュを無効化 (= 古くなったとみなす)」させるか「キャッシュをリセット」する必要があります。

「キャッシュの無効化」するにはqueryClient.invalidateQueries を使います。

  const updateName = React.useCallback(() => {
    mutate(name, {
      onSuccess: () => {
        queryClient.invalidateQueries([USER_KEY]);
      },
    });
  }, [mutate, name, queryClient]);

updateName が実行される際に、USER_KEY に紐づくキャッシュを無効化しました。

同じような形で「キャッシュをリセット」するにはqueryClient.resetQueries を使用します。 この2つはメソッドを実行したときのデータの残り方に違いがあります。 queryClient.invalidateQueries は無効化されたキャッシュが再取得完了まで残ります。 一方、queryClient.resetQueries は再取得前にキャッシュが初期状態にリセットされるので「なにも取得していない状態」になってから再取得をします。 どちらが良いかはワークフローによると思いますが、管理画面は「更新しようとしているデータ」をわかりやすくするためにqueryClient.invalidateQueries が良さそうです。

まとめ

React Query はAPI の状態を返してくれるだけではなく、強力なのはそのキャッシュ機構にあると考えます。 このキャッシュをうまく使いこなせれば、リクエストを減らすことができ、よりよいユーザー体験につながるアプリケーションにすることができます。