はじめに
新しい現場ではReactの状態管理にReduxを使っているのですが、Action CreatorやReducerなど、コード量の多さに辟易してしまうことがしばしばあります。
そんな悩みを解決する状態管理ライブラリとしてReact Queryが流行っているらしいので、このたび勉強してみることにしました。
以前書いた状態管理の記事もあわせてご覧いただければ幸いです。
React Queryとは
React Queryの大きな特徴として、サーバから取得したデータをクライアントでキャッシュとして保管できることが挙げられます。
また、新しいデータを取得したときにいつキャッシュを更新するか、なども管理することができます。
初期導入
npm install react-queryでパッケージをインストールした後、クエリとキャッシュを管理するためのqueryClientを作成し、RootのコンポーネントをQueryClientProviderでラップします。
QueryClientProviderはキャッシュやクライアント設定を下のコンポーネントに与えたり、値としてqueryClientを取得する役割をもちます。
あとはラップされたコンポーネント内でuseQuery()というHooksを使うことで、クエリを行うことができます。
import { QueryClient, QueryClientProvider } from 'react-query';
import { ReactQueryDevtools } from '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といった関係になります。
例えば、訪問済みのページに再度訪れたとき、キャッシュがあるのでデータはちゃんと表示されていますが、下のReactQueryDevToolsの画面をみるとfetchingという状態になっていることがわかります。

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>
参考資料