SWR vs React Query
(※ この記事は API およびそこから導かれる設計のしやすさ観点での比較をしています。実際にキャッシュが有効に効いたか、などについてはまた別の機会に )
動機
SPA の状態管理に必ずしも Redux いらないんじゃね問題
Redux が嬉しいのってクライアントサイドでモリモリ状態が変わる画面だけじゃん、みたいなアレ
検索結果の一覧みたいなページに Redux を使うの意味なかった
本当に必要なとこだけ使うようにしたい
ほとんどのケースで欲しいのって React 向けの API クエリキャッシュ
これまでは Redux の Store を単なるキャッシュとして使ってたが、違うよね
候補
↑ みたいな話でよく上がるオルタナティブとして SWR と React Query がある
これら 2 つを比較する。
TL;DR
キャッシュキー
SWR は URL がそのままキャッシュキーになる(少なくともそれがデフォ)
code:typescript
const { data, error } = useSWR('/api/users/1')
一方 React Query はキャッシュキーと API 取得関数が独立している。
キャッシュキーを書いてるぞ!というのが明確にわかる感じになっている
(個人的には Rails.cache.fetch を思い出した)
code:typescript
const { data, error, isLoading } = useQuery('users', 1, () => getUserById(1)) // あるいはこう
const { data, error, isLoading } = useQuery(users#1, () => getUserById(1))
SWR も react-query のような配列キャッシュキーを使えるが、その場合も URL を渡すのが SWR Way っぽい
code:typescript
const { data, error } = useSWR('/api/users', 1, (url, id) => fetch(${url}/${id}.json).then(r => r.json())) 個人的に、リアルワールドでは URL とキャッシュキーは独立していたほうが好ましいと思っている。
たとえば同じ URL でも、セッションの言語設定を見て日本語ユーザーには日本語メッセージを返すが英語ユーザーには…みたいなケースはありえる(フロントエンドで言語設定を変更したらそのキャッシュは破棄される、とかね)
SWR でもこういうケースに対処はできるんだけど( 言語設定変更後に revalidate されればきっと変わるはず )、
こういうケースでより自然に書けるのは React Query の方だと思う
(ただ不自然なケースは不自然に書かれたほうが良い、という思想もそれはそれでわかる)
code:typescript
const lang = useLang()
// lang はリクエスト URL には含まれないが、キャッシュキーには含ませたい
const { data, error, isLoading } = useQuery('items', id, lang, () => getItemById(id)) キャッシュキーの同値性
SWR は shallow equality を見る。つまり、キャッシュキーに object を使うと等価だと判定されない。
考え方としては React hooks の deps に近いのかなと思ったが、
中の等価判定アルゴリズムが Object.is ではない感じがする( コード内検索で Object.is を使ってる箇所がなかった )
ふつうに === っぽいな
code:typescript
// これはやってはいけない!!( { id: 1 } はレンダリングごとに参照が異なるのでキャッシュが効かない )
React Query はキャッシュキーに object を使うことが許容される( Deep Compare )
個人的に object をキャッシュキーにしたいシチュエーションはないと思うので、ここについては SWR の設計のほうが好ましい気がしている
( なにかのパース結果をもとにリクエストしたい、みたいなシチュエーションだと嬉しいのかもしれない )
取得関数の共通化
react-query はキャッシュキーと URL が独立しているので、取得関数の共通化については SWR より有利に思える。
たとえば、次のような関数を useQuery でも使えるし、getInitialPropsで使ってもいいし、redux-thunk で使っても良いようにできる。
code:typescript
// /api/users.ts 的な場所
export function getUserById(id: number) {
return fetch(/users/${id}.json).then(r => r.json())
}
// with react-query
// with getInitialProps
UserProfile.getInitialProps = async ({ query }) => ({
user: await getUserById(Number(query.id))
})
// with redux-thunk
export const userProfileRequested = (id: number) => dispatch => {
dispatch(
await getUserById(id)
)
}
SWR は引数に URL を渡すことが前提っぽい感じがあり、あんまりこういうタイプの関数と合わせるな、という思想を感じる。
もし自分が既存プロジェクトに swr 入れようとなった場合、↑の例にあるような /api/〇〇.ts は非推奨にして捨てていく路線を取るような気がする(それが嫌なら react-query 入れましょうって言う)。
追記
URL が前提、とまでは言い過ぎかもという指摘
( 実際公式に #GraphQL を引数に渡す例が紹介されてるので言い過ぎです ) ただ、fetcher に渡る引数を捨てる用法だと swr を使う意味が薄くなるので、キャッシュキーと fetcher の引数が一致する設計に自然に誘導される、とは言えるかなーと思う( ['user', 1] がキャッシュキーなら fetcher は user という文字列を受け取る関数であるべき、という話になる )。
それこそ OpenAPI からクライアントを自動生成してて、キャッシュキーと引数を一致させるための努力をしたくないみたいなケースはありえて、そういうケースで swr を採用すると不自然な書き方を強いられそうという印象は少し持った。