react-queryとは
react-query HPによると
Simply put, React Query makes fetching, caching, synchronizing and updating server state in your React applications a breeze.
とあります。
触ってみた肌感では、vercel/swrのような感じのhooksと、apollo clientのようなキャッシュ操作が可能な、使いやすいライブラリでした。
実際、公式にも両ライブラリとの機能の比較が分かりやすい表で示されています。
また、react-query-devtoolsを入れれば、Apollo Client Devtools のような見た目のdevtool上でキャッシュの状態を確認したり、refetchを実行したりすることができる為、非常に快適な開発体験を得られます。
使ってみる
以下は公式のexampleです。
import React from "react";
import ReactDOM from "react-dom";
import { useQuery } from "react-query";
import { ReactQueryDevtools } from "react-query-devtools";
export default function App() {
const { isLoading, error, data } = useQuery("repoData", () =>
fetch(
"https://api.github.com/repos/tannerlinsley/react-query"
).then((res) => res.json())
);
if (isLoading) return "Loading...";
if (error) return "An error has occurred: " + error.message;
return (
<div>
<h1>{data.name}</h1>
<p>{data.description}</p>
<strong>👀 {data.subscribers_count}</strong>{" "}
<strong>✨ {data.stargazers_count}</strong>{" "}
<strong>🍴 {data.forks_count}</strong>
<ReactQueryDevtools initialIsOpen />
</div>
);
}
useQuery
の第一引数はユニークなkey(文字列など)で、第二引数はfetcherです。
fetcherとは、非同期な関数で、主にはAPIを叩いたりするような処理が入るでしょう。
第一引数で渡すkeyが何に使われるのかというと、公式によれば
The unique key you provide is used internally for refetching, caching, deduping related queries.
とあるので、cache周りなどをよしなにやってくれるのに使われるようです。
逆に言えば、ここでランダムな文字列を渡してしまうとキャッシュが効かなくなる、と読めるので、このあたりはApollo Clientに似た仕組みのような感じがします。
useQuery
の返り値としては、
-
data
(fetcherの返り値をresolveした値) -
isLoading
(fetcherの返したPromiseが終了したかどうか) -
error
(fetcherがrejectされた時のError)
などがあります。
isLoading
を使ってUIを出し分けるパターンは、Apolloなどのライブラリでもおなじみですね。
しかしながら、せっかくの宣言的UIというパラダイムの中において、このような isLoading
での分岐が存在すると、コンポーネントの複雑さがドンドン増し、また、data
の返り値も T | undefined
になってしまうため、型の扱いが面倒です。
そこで登場するのがSuspenseモードです。
(一応experimentalではあるので、自己判断で使いましょう)
Suspenseモードがあると何が嬉しいのか
Suspenseの仕組みについては、React公式に説明を譲ります。
react-queryでは、類似ライブラリのvercel/swrと非常によく似たやり方で、Suspenseモードを提供しています。
方法は、
1.グローバルレベルで有効にする
// Configure for all queries
import { ReactQueryConfigProvider } from 'react-query'
const queryConfig = {
shared: {
suspense: true,
},
}
function App() {
return (
<ReactQueryConfigProvider config={queryConfig}>
...
</ReactQueryConfigProvider>
)
}
2.クエリレベルで有効にする
import { useQuery } from 'react-query'
// Enable for an individual query
useQuery(queryKey, queryFn, { suspense: true })
の2つがあります。
SuspenseモードをTypeScriptで使う上で苦労した点
このライブラリを触り始める時に少し期待していたのは、
suspenseモードで使った useQuery
の返り値はnon nullableになることでした。
Suspenseの仕組み上は、常にdefinedになるはずですが、
const { data } = useQuery(queryKey, queryFn, { suspense: true})
// このdataの型は `T | undefined` になってしまう
このようにnullableになってしまいます。
(おそらくグローバルレベルでのSuspenseモードの使い方があるせいで型の静的解析が不可能?)
そこで、これは筆者独自のワークアラウンドとして、ラッパーのhooksを作りました。
import { useQuery, QueryFunction, QueryKey, QueryResult } from "react-query";
type RequireData<T extends { data: unknown }> = T & {
data: NonNullable<T["data"]>;
};
type UseQueryWithSuspenseResult<T> = RequireData<QueryResult<T, unknown>>;
export const useQueryWithSuspense = <T extends unknown>(queryKey: QueryKey, fetcher: QueryFunction<T>): UseQueryWithSuspenseResult<T> => {
return useQuery(queryKey, fetcher, { suspense: true }) as any;
};
この useQueryWithSuspense
関数は内部で useQuery
を Suspenseモードで使っていて、なおかつ返り値が常にdefinedになります。
ただし、筆者はまだ様々なエッジケースを検証できておらず、型のつけ方にも悩んでいるので、もしより良い実装方法があればご意見を歓迎しています!
まとめ
筆者は今、ある新規プロジェクトで非同期処理・状態管理周りをどうするか考えていました。
当初はReduxを使ってローディング・エラーなどの状態を処理することを検討していましたが、
action creatorやreducerなどの実装・型付け・テストで書かなければいけない行数が非常に多いことが悩みの種でした。
いっそreact-queryとSuspenseという仕組みに全てを委ねた結果、行数をおおよそ80%ほど縮小することができました。 (※体感値)
当然ながら、アプリケーションの構成や規模によっては、こういう判断ができない場合も沢山ありますが、
もしも、
- Reduxで管理しているのは、APIのリクエスト処理周りばかりだ
- 大量の
isLoading
や、Reactのライフサイクルなどで消耗している
という場合は、一度検討してみることをオススメします!