TanStack Queryを用いたキャッシュハンドリング入門
こんにちは、エビリーでmillvi (ミルビィ)のフロントエンド開発を担当しているイケダと申します。
millviのフロントエンドではAPIのData Fetchingライブラリとして、TanStack Query(旧React Query)を採用しています
TanStack Queryを利用すると、取得したデータをキャッシュとして保持することができ、APIのリクエストを減らすことができます。
ですが、これを実現するには適切な設定を行う必要と感じたため、関連する機能について最低限まとめました。
はじめに
以下ではnext.jsを利用した場合で、話を進めます。
next.jsとTanStack Queryを利用したプロジェクトを作成するには、以下のコマンドを実行します。
# Next.js のプロジェクトを作成
$ npx create-next-app tanstack-train --ts
# TanStack Queryをインストール
$ npm i @tanstack/react-query
次に以下のようにpages/_app.tsxにQueryClientProviderを設定します。
pages/_app.tsx
import type { AppProps } from "next/app";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
const queryClient = new QueryClient();
export default function App({ Component, pageProps }: AppProps) {
return (
<QueryClientProvider client={queryClient}>
<Component {...pageProps} />
</QueryClientProvider>
);
}
これで、TanStack Queryを利用する準備が整いました。
TanStack Queryを利用したキャッシュの設定方法
TanStack Queryを用いて、APIからデータを取得するにはuseQuery Hookにデータを取得する関数を渡します。取得したデータは、キャッシュとして保持されます。
useQuery Hook の使い方
pages/index.tsxに以下のようにuseQuery Hookを設定します。
この例では、あらかじめ用意したAPIからTodoアプリのデータを取得しています。
pages/index.tsx
import { useQuery } from "@tanstack/react-query";
type Todo = {
id: number;
task: string;
isCompleted: boolean;
};
// APIからTodoデータを取得する関数
const getTodoItems = async (): Promise<Todo[]> => {
const res = await fetch("api/todo");
return res.json();
};
export default function Home() {
// useQuery Hookを利用して、Todoデータと関連する情報を取得する
const {
data,
isLoading,
// isFetching,
error
} = useQuery({
queryKey: ["todo"],
queryFn: getTodoItems,
refetchOnWindowFocus: false,
staleTime: 1000 * 60 * 5,
cacheTime: Infinity,
});
// useQuery Hookの戻り値であるisLoadingを利用することで、データの取得が完了するまでローディングを表示する
if (isLoading) return 'Loading...'
// useQuery Hookの戻り値であるerrorを利用することで、エラーが発生した場合にエラーを表示する
if (error) return 'An error has occurred'
return (
<div>
<h1>TODO ITEMS</h1>
<ul>
{data?.map((item) => (
<li key={item.id}>{item.task}</li>
))}
</ul>
</div>
);
}
useQuery Hookに渡すオブジェクトのパラメータについて説明します。
const {
data,
isLoading,
// isFetching,
error
} = useQuery({
queryKey: ["todo"],
queryFn: getTodoItems,
refetchOnWindowFocus: false,
staleTime: 1000 * 60 * 5,
cacheTime: Infinity,
});
queryKey
queryKey
には、キャッシュを一意に参照するための配列を指定します。これは、キャッシュの取り扱う際に利用されます。
これを活用するイメージとしては、クエリパラメータを指定するGETリクエストを行う際に、クエリパラメータをqueryKey
に組み込むことで、クエリパラメータが異なるリクエストごとにキャッシュを分けることができます。
完了したタスクのみ取得するクエリをキャッシュする場合のイメージ
queryKey: ["todo", { isCompleted: true }]
未完了のタスクのみ取得するクエリをキャッシュする場合のイメージ
queryKey: ["todo", { isCompleted: false }]
queryFn
queryFn
には、データを取得する関数を指定します。
指定する関数はpromiseを返す必要があることに注意してください。
const getTodoItems = async (): Promise<Todo[]> => {
const res = await fetch("api/todo");
return res.json();
};
今回はfetch APIを使って、Todoアプリのデータを取得する関数を作成しましたが、axiosなどのライブラリを使っても問題ありません。
refetchOnWindowFocus
refetchOnWindowFocus
は、ブラウザのタブがフォーカスされた際に、データを再取得するかどうかを指定します。
staleTime
staleTime
には、キャッシュの有効期限をmsで指定します。
この値を超える時間が経過すると、キャッシュが古くなったものとして扱われ、次回データを再取得するようになります。
ここで重要なのは、「キャッシュが古くなった物として扱う」という表現で、実際にはデータを再取得するまでは古いキャッシュが利用されます。
そして、データを再取得すると、新しいキャッシュに置き換えられ、画面に反映されます。
サンプルの例だと再取得中はisLoading
はfalse
のままで、となり「Loading...」画面が表示されることはありません。
staleTime
のデフォルト値は0となっているため、デフォルトのまま利用する際は注意が必要です。
余談ですが、実際に動かしてみたところrefetchOnWindowFocus
がtrueの場合でも、staleTime
内であればデータは再取得されないようです。
staleTime
を適切に設定していれば、refetchOnWindowFocus
はそれほど気にする必要はないかもしれません。
cacheTime
cacheTime
には、キャッシュの保持期限をmsで指定します。
この値を超える時間が経過すると、キャッシュは削除されます。
staleTime
との違いは、「キャッシュが削除される」という点で、キャッシュ未所持の状態となるため、サンプルの例だと、データが再取得されるまでisLoading
がtrue
となりLoading画面が表示されます。
例では、Infinityを指定しているため、キャッシュは削除されません。
デフォルト値は5分となっているため、5分以上経過するとキャッシュは削除されます。
基本的には、staleTime
よりもcacheTime
を長く設定しておけばそれほど問題はないと思います。
キャッシュの更新方法について
基本的に、useQuery Hookで取得したデータのキャッシュは、staleTime
経過するまで更新されません。
しかし、実際にはデータの追加・更新・削除などの操作を行った際に、キャッシュを更新する必要があります。
ここではuseQuery Hookで取得したキャッシュを任意のタイミングで更新・削除する方法を説明します。
キャッシュの更新・削除には、useQuery Hookから呼び出せるqueryClient
を利用します。
queryClient
const queryClient = useQueryClient();
queryClientにはいくつかの便利なメソッドが用意されています。
それぞれのメソッドは、キャッシュやAPIとのやりとりを操作するための機能を提供しています。
ここでは、invalidateQueries
、refetchQueries
、removeQueries
、resetQueries
について、それぞれの違いを説明します。
invalidateQueries
queryClient.invalidateQueries({ queryKey: ["todo"] });
invalidateQueries
メソッドは、指定されたqueryKey
に関連するすべてのキャッシュデータを無効にするために使用されます。
無効化されたキャッシュを扱うクエリが現在レンダリング中のコンポーネント内で使用されている場合、invalidateQueries
メソッドが呼び出された時点でバックグラウンドで再取得されます。
挙動としては、キャッシュが更新されるまでは、古いキャッシュが利用されるので、Loading...
の画面が表示されることはありません。
クエリが現在レンダリング中のコンポーネント内で使用されていない場合は、次に使用されるタイミングで同様に再取得されます。
またqueryKey
の指定は前方一致で指定することができ、invalidateQueries({ queryKey: ["todo"] })
とすることで、先に例で示したqueryKey: ["todo", { isCompleted: true }]
やqueryKey: ["todo", { isCompleted: false }]
に関連するキャッシュデータを無効にすることができます。
キャッシュの無効化をqueryKey: ["todo"]
に限定したい場合は、invalidateQueries({ queryKey: ["todo"], exact: true })
とすることで、完全一致で指定することもできます。
refetchQueries
queryClient.refetchQueries({ queryKey: ["todo"] });
refetchQueries
メソッドは、指定されたqueryKey
に関連するすべてのキャッシュデータを再取得するために使用されます。
invalidateQueries
メソッドとの違いは、refetchQueries
メソッドは、指定したqueryKey
に関連するクエリが現在レンダリング中のコンポーネント内で使用されているかどうかに関わらず、refetchQueries
メソッドが呼びだされたタイミング再取得を行う点です。
つまり、現在表示しているページに関連しないクエリのキャッシュも更新できるということです。
これは、別のページに遷移した時、すでにキャッシュが更新されているので、古いデータが表示されることを防ぐことができる一方で、場合によっては不要なクエリを大量に再取得することになるため、注意が必要です。
resetQueries
queryClient.resetQueries({ queryKey: ["todo"] });
resetQueries
メソッドは、指定されたqueryKey
に関連するすべてのクエリのキャッシュデータをリセットするために使用されます。
挙動としては、一度キャッシュを削除した後、再取得を行うので、再取得が完了するまでは、useQuery HookのisLoaded
が再びtrue
の状態になります。
resetQueries
メソッドは、invalidateQueries
メソッドと同様に、現在レンダリング中のコンポーネント内で使用されているクエリに関しては、resetQueries
メソッドが呼び出された時点で再取得され、そうでないクエリに関しても次に使用されるタイミングで再取得されます。
removeQueries
queryClient.removeQueries({ queryKey: ["todo"] });
removeQueries
メソッドは、指定されたqueryKey
に関連するすべてのクエリのキャッシュデータを削除するために使用されます。
resetQueries
メソッドとの違いは、removeQueries
メソッドは、キャッシュの削除のみを行い、再取得は行わない点です。
TanStack Queryを用いた作成・更新・削除について
TanStack Queryにはデータの作成・更新・削除を行うためのuseMutation Hookが用意されています。
キャッシュの更新と組み合わせて利用することも多いと思うので、ここで簡単に紹介しておきます。
以下はTODOの追加を行う例です。
まず、TODOを追加するAPIのリクエストを行う関数を作成します。
const addTodo = async ({
task,
}:
{
task: string;
}) => {
const res = await fetch("api/todo", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
task,
}),
});
if (!res.ok) {
throw new Error(`${res.status} ${res.statusText}`);
}
return res.json();
};
これに対して、useMutation Hookを利用して、TODOを追加する関数を作成します。
const queryClient = useQueryClient()
const addMutation = useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["todo"] });
},
onError: (error) => {
console.log(error);
},
});
const handleAdd = async () => {
await addMutation.mutate({ task: task });
};
上記のように、成功時にqueryClientのinvalidateQueries
メソッドを呼び出すことで、TODOの一覧を取得するクエリのキャッシュを更新することができます。
まとめ
今回は、TanStack Queryのデータ取得とキャッシュの取り扱いについて解説しました。
キャッシュをうまく扱えると、アプリケーションのパフォーマンスが向上するだけでなく、ユーザーのストレスを軽減できるので、ぜひ考慮してみてください。
今回の記事作成にあたって、作成したコードははこちらです。
ただし、あくまでも動作確認用のコードなので、その点ご了承ください。
また、今回は扱わなかった機能についても、公式ドキュメントに詳しく記載されているので、興味がある方はぜひご覧ください。
最後に
弊社でのプロダクト開発や技術に少しでも興味を持っていただけた方は、以下のリンクからお気軽にご連絡ください。
Discussion