見出し画像

いまさら聞けないReact Queryの基本事項(Stale, Cache, Prefetchingなど)

はじめに

こんにちは、エンジニアのすずきです。

現在、OPTEMOのフロントエンドはReact + TypeScript + Reduxで構成されています。
Reduxはグローバルな状態管理とデータ取得(非同期処理)に使っているのですが、どちらかというとデータ取得の用途が主となっています。

Reduxのような重たいボイラープレートのライブラリにミドルウェア(redux-thunk)層を追加するという複雑な構成が、少人数でスピード感をもって開発していく上で最適なのか?将来的に技術負債を生む可能性はないのか?ということを考える中で、データ取得の代替案として、React Query(TanStack Query)やSWRのような専用ライブラリを検討していくことになりました。

React QueryとSWRのどちらを選択するかはまだ決まっていないのですが、1年くらい前に少しだけReact Queryの勉強をしたことがあったので、記憶を取り戻しつつ、基本事項をできるだけ簡潔にまとめてみることにしました。

React Queryとは

React Queryの大きな特徴として、サーバから取得したデータをクライアントでキャッシュとして保管できることが挙げられます。
新しいデータを取得したときにいつキャッシュを更新するか、なども管理することができ、UIにあわせたレンダリングの制御などを自由に行うことができます。

初期導入

npmかyarnでパッケージをインストールします(React v16.8以上)。

npm install @tanstack/react-query
yarn add @tanstack/react-query

クエリとキャッシュを管理するためのqueryClientを作成し、RootのコンポーネントをQueryClientProviderでラップします。
QueryClientProviderはキャッシュやクライアント設定をラップしたコンポーネントに与えたり、値としてqueryClientを取得する役割をもちます。

あとはラップされたコンポーネント内でuseQuery()というHooksを使うことで、クエリを行うことができます。

import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

const queryClient = new QueryClient();

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <div className="App">
        <Posts />
      </div>
      <ReactQueryDevtools />
    </QueryClientProvider>
  );
}

ラップされたコンポーネントの中にReactQueryDevToolというものがありますが、これはReact Queryで管理しているデータの変化を確認するための開発者用ツールです。

Fetching

以下のようなデータをフェッチする関数からuseQuery()でデータを取得してみます。

async function fetchPosts(pageNum) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/posts?_limit=10&_page=${pageNum}`
  );
  return response.json();
}

useQuery()の第一引数にはkeyを、第二引数にはフェッチ関数をコールバックとして配置します。
すると、dataをはじめとした様々なプロパティを取得することができます。

  const { data, isError, error, isLoading, isFetching } = useQuery(
    'posts',
    () => fetchPosts(currentPage)
  );

isFetchingとisLoadingの違い

isFetchingはクエリ関数がデータをフェッチするまでの状態、isLoadingはisFetchingの状態に加えてキャッシュデータももっていない状態を指します。
つまり、キャッシュをもっていればisLoadingはfalseとなり、isFetching ⊃ isLoadingといった関係になります。

Stale timeとCache timeの違い

以上の画面で、fetchingの右側にstaleとありますが、これもReact Queryのキャッシュ機構を理解する上で大事なキーワードとなります。

Stale timeというのは、キャッシュデータが古くなったとみなす時間のことをいいます。
Stale timeはデフォルトで0 sなのですが、以下のようにstaleTime: 1000などと設定することで、1000 ms以内に再訪問したページであればデータを新しいもの(fresh)とみなし、フェッチを行わずにキャッシュを利用するようになります。
1000 msを超えた場合にはキャッシュが使えなくなり、データを再フェッチします。

  const { data, isError, error, isLoading, isFetching } = useQuery(
    'posts',
    () => fetchPosts(currentPage),
    {
      staleTime: 1000,
    }
  );

一方、Cache timeはデータをキャッシュする時間のことをいいます。
デフォルトの設定は5分(300000 ms)となっています。
staleTime: 0, cacheTime: 300000のとき、同じページを再訪問するとキャッシュデータが画面に表示されますが、staleTimeの時間が過ぎてキャッシュデータは古いものとみなされるため、バックグラウンドで再フェッチが実行されます。

useQuery()のkey

useQuery()の第一引数に単一のkeyを与えるとき、再フェッチのトリガーとなるのは以下のようなケースです。

  • コンポーネントの再マウント

  • ウィンドウの再フォーカス

  • 再フェッチ関数の実行

これらのケースから外れてしまうと、クエリが実行されず再フェッチも行われません。
この問題を解決するためには、keyを依存配列として扱い、中身の値が変わったときにuseQuery()を実行するようにします。

例えば、ページネーションでcurrentPageが変わったときにuseQuery()を実行させるとしたら、以下のような記述となります。

  const { data, isError, error, isLoading, isFetching } = useQuery(
    ['posts', currentPage],
    () => fetchPosts(currentPage),
    {
      staleTime: 2000,
    }
  );

Prefetching

Prefetchingとは、予測されるデータをキャッシュに書き込んでおき、ページを開いてフェッチを行っている間、キャッシュデータを表示されるようにすることです。

useEffect()にprefetchQuery()を仕込んでおくことで、現在のページを開いたときに、次のページのデータを['posts', nextPage]のkeyにキャッシュしています。

  const queryClient = useQueryClient();

  useEffect(() => {
    if (currentPage < maxPostPage) {
      const nextPage = currentPage + 1;
      queryClient.prefetchQuery(['posts', nextPage], () =>
        fetchPosts(nextPage)
      );
    }
  }, [currentPage, queryClient]);

Mutation

useQuery()ではデータの取得処理を行いましたが、書込処理を行うためのHooksとしてuseMutation()というものもあります。
useQuery()との違いとしては以下のものがあります。

  • mutate関数を返す

  • keyは不要

  • isLoadingはあるがisFetchingはなし

  • デフォルトでリトライなし(クエリはデフォルト3回)

例えば、以下のようなデータ削除のための関数を考えます。

async function deletePost(postId) {
  const response = await fetch(
    `https://jsonplaceholder.typicode.com/postId/${postId}`,
    { method: 'DELETE' }
  );
  return response.json();
}

useMutation()からdeleteMutationを定義します。

const deleteMutation = useMutation((postId) => deletePost(postId));

Deleteボタンに仕込むことで、データの削除を実行することができます。

<button onClick={() => deleteMutation.mutate(post.id)}>Delete</button>

さいごに

ジェイタマズではエンジニアを募集しています。
React/TypeScript/NestJSでモダンな開発をしてみたい!会社やサービスに興味がある!という方がいらっしゃいましたら、ぜひ気軽にカジュアル面談しましょう!

この記事が気に入ったらサポートをしてみませんか?