はじめに
新しい現場では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>
参考資料