【2023年】SWR & TanStack Query比較
はじめに
今回はSWRとTanStack Queryの比較によってそれぞれの特徴と違いを整理したいと思います。背景としてネット上にある両者の比較記事は2022年以前のものが多く、当時に比べSWR2.0がリリースされたことなどで比較の観点が変化したように感じました。改めて整理することで技術選定の参考になればと思います。
前提
今回は以下のバージョンを前提にします。(2023/08/26時点でLatest)
- SWR v2.2.0
- TanStack Query v4.34.0
また、私自身はTanStack Queryを業務で1年ほど扱ったことがありSWRは全く経験がない状態です。この記事はどちらが優れているかを示すためのものではなく、あくまで客観的に比較することを目的にしています。
目次
ここでは以下の3つの観点から比較を行い考察をします。
- interfaceでの比較
- 機能面での比較
- キャッシュ観点での比較
interfaceでの比較
TanStack QueryのuseQuery
とSWRのuseSWR
におけるinterfaceの違いを比較します。それぞれの引数を確認します。
useQuery(TanStack Query)の場合
export function useQuery({ queryKey, queryFn, ...options })
useSWR(SWR)の場合
export function useSWR(key, fetcher, options)
比較するとキャッシュ識別のためのkeyとデータフェッチ関数を渡すことが一致ししており、そのkeyの扱いが若干異なります。
Keyの扱い方
TanStack Queryの場合
TanStack QueryのqueryKeyは以下のように定義されています。
- queryKey: unknown[]
- Required
- The query key to use for this query.
- The query key will be hashed into a stable hash. See Query Keys for more information.
- The query will automatically update when this key changes (as long as enabled is not set to false).
TanStack QueryではqueryKeyの型がunknown[]
と定義されています。また、queryKeyはqueryFnとは独立して使用することができ、一意であればどんな値を設定することもできます。
効率的なKeyの管理方法は公式のブログにて解説されています。
SWRの場合
SWRの場合はkeyに設定した値がfetcherに渡されます。
そのため、keyに設定する値はfetcherで使用するためのurlを期待します。
const fetcher = (...args) => fetch(...args).then(res => res.json())
function App () {
const { data, error } = useSWR('/api/data', fetcher)
// ...
}
keyにfetcherで使用するためのurlを渡す場合も、文字列に制限はされていません。keyの型定義は以下のようになっており、SWR1.1.0からはオブジェクトのようなキーは内部で自動的にシリアライズされるように設計されています。
type ArgumentsTuple = readonly [any, ...unknown[]];
type Arguments = string | ArgumentsTuple | Record<any, any> | null | undefined | false;
type Key = Arguments | (() => Arguments);
追記:fetcherをリモートデータ取得の用途で使用しない場合、明示的にnullを渡すことでkeyをurlに限定せずGlobal Stateとして使用する方法もあります。
const { data: count, mutate: setCount } = useSWR('count', null, {
initialData: initialCount,
});
これらの比較からkeyの管理における柔軟性はTanStack Queryの方があることがわかります。しかし、柔軟性があるがゆえにその管理が複雑になってしまうデメリットもあります。TanStack Queryを採用する際にはkeyの管理に一定のルールを敷くことが重要になってきます。
機能面での比較
機能面で両者の比較を行います。TanStack QueryのドキュメントにてSWR、Apollo Client、RTK-Query、React Routerとの比較対応表を掲載してくれているのでまずはこちらを参考にします。
バンドルサイズはSWRの方が軽い
結論、バンドルサイズの観点ではSWRの方がTanStack Queryよりも3倍ほど軽くなっています。
- TanStack Query
- 13.0KB
- SWR
- 4.4KB
SWRの方がTanStack Queryに比べ機能を必要最小限に整備していることから軽量化されていることが伺えます。当然TanStack Queryはその分機能が多くなっています。
Query Hooksによる返り値の種類
TanStack QueryとSWRを比較したときにQuery Hooksの返り値の種類に差があります。ここで言うQuery Hooksとは、TanStack QueryであればuseQuery
、SWRであればuseSWR
のことを指します。
TanStack Queryの場合
const {
data, dataUpdatedAt, error, errorUpdateCount,
errorUpdatedAt, failureCount, failureReason, fetchStatus,
isError, isFetched, isFetchedAfterMount, isFetching,
isInitialLoading, isLoading, isLoadingError, isPaused,
isPlaceholderData, isPreviousData, isRefetchError, isRefetching,
isStale, isSuccess, refetch, remove, status
} = useQuery()
TanStack Queryの特徴としてuseQueryの返り値が充実していることが挙げられます。返却される値が多ければ多いほど非同期の処理を柔軟に行うことができるのはメリットでしょう。その一方で、全ての返り値を適切に扱うには相当の学習コストがかかることも事実です。
例えばdataの状態に関心のあるstatus
とfetchの状態に関心のあるfetchStatus
がそれぞれ分けて管理されていることはSWRと比較したときの特徴とも言えます。
SWRの場合
const {
data,
error,
isLoading,
isValidating,
mutate
} = useSWR()
TanStack Queryと比較することで一目瞭然ですが、SWRの場合useSWRから返却される値はたったの5つです。SWRが必要最小限の機能になっていることがここからも伺えます。
双方を比較するとSWRはあまりにも扱える状態が少なく感じてしまいます。しかし、非同期の状態を管理する上では5つの値のみあれば十分実装ができると捉えることもできます。
複雑な非同期の状態を扱いたいのか、最低限でシンプルに実装したいのかという観点でも技術選定が分かれてきそうです。
Query Hooksオプションの種類
TanStack QueryとSWRではQuery Hooksに渡せるオプションにも特徴が分かれます。オプションの違いには様々ありますが、ここではTanStack Queryに存在するenabled
オプションとselect
オプション、onSuccess
系オプションに着目してSWRと比較します。
enabled
オプション
TanStack Queryにはenabled
というオプションがあり、依存する他クエリの取得を待ってから該当クエリを発火させることができます。以下の例ではユーザの取得を待ってから、取得したユーザIDをもとにprojectのクエリを発火させています。
// Get the user
const { data: user } = useQuery({
queryKey: ['user', email],
queryFn: getUserByEmail,
})
const userId = user?.id
// Then get the user's projects
const {
status,
fetchStatus,
data: projects,
} = useQuery({
queryKey: ['projects', userId],
queryFn: getProjectsByUser,
// The query will not execute until the userId exists
enabled: !!userId,
})
これによってクエリ同士に依存性を持たせることが可能になります。
一方、SWRではenabled
のオプションは存在しません。クエリ同士に依存性を持たせたい場合にはuseSWRのkeyに関数を渡すことで同様の実装ができます。
function MyProjects () {
const { data: user } = useSWR('/api/user')
const { data: projects } = useSWR(() => '/api/projects?uid=' + user.id)
// 関数を渡す場合、SWRは返り値を`key`として使用します。
// 関数がスローまたはfalsyな値を返す場合、
// SWRはいくつかの依存関係が準備できてないことを知ることができます。
// この例では、`user.id`は`user`がロードされてない時にスローします。
if (!projects) return 'loading...'
return 'You have ' + projects.length + ' projects'
}
select
オプション
TanStack Queryにはselect
というオプションがあり、queryFnから取得したデータをさらに加工して扱うことができます。
公式でもfetchしてきたデータを加工する際にはselectオプションを使用することを推奨しています。
export const useTodosQuery = () =>
useQuery({
queryKey: ['todos'],
queryFn: fetchTodos,
select: (data) => data.map((todo) => todo.name.toUpperCase()),
})
一方でSWRにはselectオプション、つまりfetchしてきたデータの加工を行うオプションがないのでuseSWRの外で良しなに加工が必要になりそうです。
onSuccess系オプション
ここで言うonSuccess系オプションとは、onSuccess
onError
などの非同期結果に応じた処理を行うためのオプションです。
TanStack Queryでもv4まではuseQueryのオプションにonSuccess系は存在していました。しかし、v5からはこれらのオプションが削除されます。
TanStack QueryにおいてonSuccess系オプションを使用することはバグの温床になるとして廃止が決定しました。詳しくは公式のブログを参照ください。
その一方でSWRの場合はonSuccess系オプションが充実しているので、さまざまな非同期の結果に応じて処理を施すことが可能です。
onLoadingSlow(key, config): // リクエストの読み込みに時間がかかりすぎる場合のコールバック関数
onSuccess(data, key, config): // リクエストが正常に終了したときのコールバック関数
onError(err, key, config): // リクエストがエラーを返したときのコールバック関数
onErrorRetry(err, key, config, revalidate, revalidateOps): // エラー再試行のハンドラー
onDiscarded(key): // レースコンディションによりリクエストが無視されたときのコールバック関数
Query Hooksオプションについての比較から同じ目的でも実装方法に違いがあることがわかりました。
TanStack Quryの場合はオプションが充実しているので、それを利用してさまざまな非同期の処理を行うことができます。しかし、複雑性が増したが故にonSuccess系オプションのように意図しないバグの温床になりうることもありました。
それに対してSWRはシンプルである一方、データフェッチ後の加工をuseSWRの外で行う必要があるなどの物足りなさがあります。
TanStack Queryは破壊的変更が多い印象
TanStack Queryはメジャーバージョンアップに伴う破壊的な変更が多い印象があります。ここではstatus
を例に挙げます。
TanStack Query v3の時のstatus
には4種類の状態がありました。
idle
loading
error
success
しかし、v4に上がるに従ってidle
の状態がstatus
から削除され以下の3種類になりました。
loading
error
success
さらにはこの変更に伴って、isLoading
フラグの定義にも影響を及ぼしています。idle
が削除されたことで、loading
の状態にidle
状態が包含され、loading
の定義が広くなったためです。
また、status
の変更はv4では収まらず、v5に上がる際にも変更が入ります。
v5では、今までloading
という命名で扱っていたフラグがpending
に変わり、status
の状態は以下の3種類になります。
pending
error
success
それに伴い、isLoading
もisPending
と命名が変わります。今回は命名だけの変更ではありますが、TanStack Query自体がその状態の扱いに対して確固たる答えを持っていないことがこれらの変更から読み取れます。
このように、メジャーバージョンの更新のたびに破壊的な変更が入り、非同期の状態の定義が変わってしまうことは長期的な保守という観点ではマイナスになり得ます。多機能で複雑な非同期の状態を扱えることはメリットですが、TanStack Queryを選定する場合、これらの変更にも追従していく必要があります。
Mutation Hooksの比較
SWR1.0時点ではTanStack QueryのuseMutationにあたるHooksは存在しておらず、Mutation Hooksの有無が双方の違いとして語られていた印象でした。しかし、SWR2.0によってuseSWRMutationと言うHooksが追加され、これがTanStack QueryのuseMutationの役割に該当します。
TanStack Queryの場合
useMutationのinterfaceは以下の通りになっています。
const {
data, error,
isError, isIdle,
isLoading, isPaused,
isSuccess, failureCount,
failureReason, mutate,
mutateAsync, reset, status,
} = useMutation({ mutationFn, ...options })
useQuery同様に多様な値を返却しており、非同期処理の扱いにおいて非常に柔軟性があります。必須の引数はmutationFn
で更新系の関数を渡すことを期待しています。
SWRの場合
useSWRMutationの場合は、TanStack Queryと比較してkeyとfetcherを必須の引数に受け取る点が異なっています。
また、useSWRと同様に必要最小限に返り値とオプションが設計されています。
const {
data, error,
trigger, reset, isMutating
} = useSWRMutation(key, fetcher, ...options)
useSWRMutationがkeyを必須の引数にしている理由に、keyに紐づくキャッシュをmutate(キャッシュの再検証)する目的があります。何かしらの更新処理をしたということは、既存のキャッシュが古くなったと考えることができます。
SWRの場合、キャッシュのkeyとuseSWRMutationのkeyを紐づけることで更新処理の後に自動でキャッシュを更新する設計になっています。
自動でキャッシュの再検証を行うかどうかはrevalidate
オプションで制御されているので、上記の挙動を無効にすることもできます。(デフォルトはtrue)
キャッシュ観点での比較
最後にキャッシュの観点で双方を比較します。TanStack QueryもSWRもどちらもキャッシュ機構を持っており、それぞれの違いを把握することは非常に重要です。
TanStack Queryでのキャッシュ戦略
TanStack Queryのキャッシュ戦略を理解するにはstaleTime
とcacheTime
の存在が重要になってきます。staleTime
とは、キャッシュをstale(古くなったとみなす)状態にするまでの期間です。一方、cacheTime
はキャッシュをガベージコレクション(メモリ領域の開放)するまでの時間です。
staleTimeはデフォルトで0に設定されており、すぐにキャッシュはstale状態になります。逆にstaleTimeをInfinityに設定した場合には、キャッシュが常にfresh状態になるので不要なデータフェッチを防いでリクエストを必要最小限にすることができます。しかし適切なrevalidate処理をしないと最新の状態を取得できないので注意です。
このようにTanStack Queryではキャッシュの生存期間を厳密に設定することができます。
SWRでのキャッシュ戦略
SWRではTanStack Queryのようにキャッシュの生存期間を設定するオプションはありません。基本的にキャッシュの自動再検証は以下のタイミングで行われます。(これらのオプションはTanStack Queryにも共通します。)
- revalidateOnMount
- コンポーネントのマウント時
- revalidateOnFocus
- ウィンドウがフォーカスされた時
- revalidateOnReconnect
- ブラウザがネットワーク接続を回復した時
もしも定期的にデータを取得して最新性を保証したい場合はrefreshInterval
オプションにてポーリングの間隔を設定することが有効です。
これらの比較からも、staleTimeによってキャッシュの生存期間を細かく制御し非同期の状態を調整できる点がTanStack Queryの強みでありSWRとの違いと言えます。
revalidateの方法が異なる
TanStack QueryとSWRを比較してrevalidate処理の方法に違いがあります。TanStack Queryでは、更新処理を行い古くなったキャッシュを更新する方法として、useMutationのonSuccess時にrevalidate処理をすることが一般的です。
import { useMutation, useQueryClient } from '@tanstack/react-query'
const queryClient = useQueryClient()
// When this mutation succeeds, invalidate any queries with the `todos` or `reminders` query key
const mutation = useMutation({
mutationFn: addTodo,
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ['todos'] })
queryClient.invalidateQueries({ queryKey: ['reminders'] })
},
})
その一方で、SWRのuseSWRMutationでは第一引数に渡すkeyがキャッシュのkeyと関連付いており該当するキャッシュの再検証を行なってくれます。 これによってTanStack Queryの様に明示的にrevalidate処理をする必要がなくなります。
さらには更新処理のリクエストが更新後のデータを直接返す場合、useSWRMutationのpopulateCacheオプションを活用することで余分なデータフェッチを抑えてキャッシュを更新させることもできます。
useSWRMutation('/api/todos', updateTodo, {
populateCache: (updatedTodo, todos) => {
// リストをフィルタリングして更新後のアイテムと一緒に返します
const filteredTodos = todos.filter(todo => todo.id !== '1')
return [...filteredTodos, updatedTodo]
},
// API が更新後の情報を返してくれているので、
// 再検証は必要ありません
revalidate: false
})
設計面での考察
SWRの設計について一点気になったのはキャッシュの管理がAPIの設計に依存している点です。先ほどuseSWRとuseSWRMutationのkeyが一致していることで、更新時のmutateを自動で行いデータを最新に保つ仕組みを説明しました。
これはつまりGET処理のエンドポイントと更新系処理のエンドポイントが一致している前提であり、APIの設計がこの通りでなければ意図通りにキャッシュを更新してくれません。
いわば以下の図のような依存関係になっている様に思います。
もちろんurlが一致しておらずkeyを関連づけることができなくても、明示的にmutate
APIを使ってキャッシュを更新させることはできます。
SWRの選定にはアプリケーションの設計がSWRの設計とマッチするかという観点でも検討が必要に感じました。
まとめ
今回はSWRとTanStack Queryを比較してきました。当然この記事の中では扱いきれないほどの違いがありますが、総じて言えることはSWRの方がシンプルに設計されていることです。それに対してTanStack Queryは多機能で様々な非同期の状態を扱うことができます。
一つの参考として、まずはSWRの導入を検討するのがいいかと思いました。しかし、SWRの設計がアプリケーションにマッチしなかったり、非同期処理における物足りなさを感じた場合にはTanStack Queryの採用を検討するのも良いでしょう。TanStack Queryはより柔軟なキャッシュの管理と非同期の状態を扱えます。
Discussion
とても見通しのいい記事で参考になりました.
一点,グリッチのようなものとしてSWRに関して補足します.
key
がfetcher
に依存するのはその通りなのですが,逆に言えばfetcher
を明示的にnull
にすればkey
は任意になるのでURL以外も扱うことができます.コメントいただきありがとうございます!
SWRをGlobal Stateとして使用する方法もあるんですね。勉強になりました!
いただいた内容をもとに本記事にも追記させていただいております🙇♂️
必ずしも
fetcher
の引数がurl
でなければならないということはないので、たとえば
こういうこともできます。
key
とurl
が一致している必要はない。というところから、このような前提もなくなります。
コメントいただきありがとうございます!
確かに設計次第ではfetcherの引数がurlに限定されることはないですね。
大変勉強になりました🙇♂️
とても勉強になりました。ありがとうございます。
こちらについてですが、Middlewareを定義することでデータの前処理と後処理が可能です!
コメントいただき、ありがとうございます!
確かにSWRのMiddlewareでも前後の処理は可能ですね。その一方で各リクエストごとに異なるデータの加工処理をする観点でMiddlewareが最適かどうかというのは判断の余地がある気がしました!