🛰️

【2023年】SWR & TanStack Query比較

2023/09/14に公開
4

はじめに

今回はSWRとTanStack Queryの比較によってそれぞれの特徴と違いを整理したいと思います。背景としてネット上にある両者の比較記事は2022年以前のものが多く、当時に比べSWR2.0がリリースされたことなどで比較の観点が変化したように感じました。改めて整理することで技術選定の参考になればと思います。

https://tanstack.com/query/latest

https://swr.vercel.app/ja

前提

今回は以下のバージョンを前提にします。(2023/08/26時点でLatest)

  • SWR v2.2.0
  • TanStack Query v4.34.0

また、私自身はTanStack Queryを業務で1年ほど扱ったことがありSWRは全く経験がない状態です。この記事はどちらが優れているかを示すためのものではなく、あくまで客観的に比較することを目的にしています。

目次

ここでは以下の3つの観点から比較を行い考察をします。

  1. interfaceでの比較
  2. 機能面での比較
  3. キャッシュ観点での比較

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の管理方法は公式のブログにて解説されています。

https://tkdodo.eu/blog/effective-react-query-keys

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);

https://swr.vercel.app/ja/docs/arguments

追記:fetcherをリモートデータ取得の用途で使用しない場合、明示的にnullを渡すことでkeyをurlに限定せずGlobal Stateとして使用する方法もあります。

const { data: count, mutate: setCount } = useSWR('count', null, {
  initialData: initialCount,
});

https://zenn.dev/tak_iwamoto/articles/39aefec675c323#swrを使用した状態管理

これらの比較からkeyの管理における柔軟性はTanStack Queryの方があることがわかります。しかし、柔軟性があるがゆえにその管理が複雑になってしまうデメリットもあります。TanStack Queryを採用する際にはkeyの管理に一定のルールを敷くことが重要になってきます。

機能面での比較

機能面で両者の比較を行います。TanStack QueryのドキュメントにてSWR、Apollo Client、RTK-Query、React Routerとの比較対応表を掲載してくれているのでまずはこちらを参考にします。

https://tanstack.com/query/latest/docs/react/comparison

バンドルサイズは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と比較したときの特徴とも言えます。

https://tanstack.com/query/latest/docs/react/reference/useQuery

SWRの場合

const { 
  data, 
  error, 
  isLoading, 
  isValidating, 
  mutate 
} = useSWR()

TanStack Queryと比較することで一目瞭然ですが、SWRの場合useSWRから返却される値はたったの5つです。SWRが必要最小限の機能になっていることがここからも伺えます。

https://swr.vercel.app/ja/docs/api

双方を比較すると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,
})

https://tanstack.com/query/v4/docs/react/guides/dependent-queries

これによってクエリ同士に依存性を持たせることが可能になります。

一方、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'
}

https://swr.vercel.app/ja/docs/conditional-fetching

selectオプション

TanStack Queryにはselectというオプションがあり、queryFnから取得したデータをさらに加工して扱うことができます。

https://tkdodo.eu/blog/react-query-data-transformations

公式でも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からはこれらのオプションが削除されます。

https://twitter.com/TkDodo/status/1647341498227097600

TanStack QueryにおいてonSuccess系オプションを使用することはバグの温床になるとして廃止が決定しました。詳しくは公式のブログを参照ください。

https://tkdodo.eu/blog/breaking-react-querys-api-on-purpose

その一方で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の定義が広くなったためです。

https://tanstack.com/query/v4/docs/react/guides/migrating-to-react-query-4#the-idle-state-has-been-removed

また、statusの変更はv4では収まらず、v5に上がる際にも変更が入ります。

v5では、今までloadingという命名で扱っていたフラグがpendingに変わり、statusの状態は以下の3種類になります。

  • pending
  • error
  • success

それに伴い、isLoadingisPendingと命名が変わります。今回は命名だけの変更ではありますが、TanStack Query自体がその状態の扱いに対して確固たる答えを持っていないことがこれらの変更から読み取れます。

https://tanstack.com/query/v5/docs/react/guides/migrating-to-v5#status-loading-has-been-changed-to-status-pending-and-isloading-has-been-changed-to-ispending-and-isinitialloading-has-now-been-renamed-to-isloading

このように、メジャーバージョンの更新のたびに破壊的な変更が入り、非同期の状態の定義が変わってしまうことは長期的な保守という観点ではマイナスになり得ます。多機能で複雑な非同期の状態を扱えることはメリットですが、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で更新系の関数を渡すことを期待しています。

https://tanstack.com/query/v5/docs/react/reference/useMutation

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)

https://swr.vercel.app/ja/docs/mutation

キャッシュ観点での比較

最後にキャッシュの観点で双方を比較します。TanStack QueryもSWRもどちらもキャッシュ機構を持っており、それぞれの違いを把握することは非常に重要です。

TanStack Queryでのキャッシュ戦略

TanStack Queryのキャッシュ戦略を理解するにはstaleTimecacheTimeの存在が重要になってきます。staleTimeとは、キャッシュをstale(古くなったとみなす)状態にするまでの期間です。一方、cacheTimeはキャッシュをガベージコレクション(メモリ領域の開放)するまでの時間です。

staleTimeはデフォルトで0に設定されており、すぐにキャッシュはstale状態になります。逆にstaleTimeをInfinityに設定した場合には、キャッシュが常にfresh状態になるので不要なデータフェッチを防いでリクエストを必要最小限にすることができます。しかし適切なrevalidate処理をしないと最新の状態を取得できないので注意です。

このようにTanStack Queryではキャッシュの生存期間を厳密に設定することができます。

https://tanstack.com/query/v4/docs/react/guides/caching

SWRでのキャッシュ戦略

SWRではTanStack Queryのようにキャッシュの生存期間を設定するオプションはありません。基本的にキャッシュの自動再検証は以下のタイミングで行われます。(これらのオプションはTanStack Queryにも共通します。)

  • revalidateOnMount
    • コンポーネントのマウント時
  • revalidateOnFocus
    • ウィンドウがフォーカスされた時
  • revalidateOnReconnect
    • ブラウザがネットワーク接続を回復した時

もしも定期的にデータを取得して最新性を保証したい場合はrefreshIntervalオプションにてポーリングの間隔を設定することが有効です。

https://swr.vercel.app/ja/docs/revalidation

これらの比較からも、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'] })
  },
})

https://tanstack.com/query/v4/docs/react/guides/invalidations-from-mutations

その一方で、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
})

https://swr.vercel.app/ja/docs/mutation#update-cache-after-mutation

設計面での考察

SWRの設計について一点気になったのはキャッシュの管理がAPIの設計に依存している点です。先ほどuseSWRとuseSWRMutationのkeyが一致していることで、更新時のmutateを自動で行いデータを最新に保つ仕組みを説明しました。

これはつまりGET処理のエンドポイントと更新系処理のエンドポイントが一致している前提であり、APIの設計がこの通りでなければ意図通りにキャッシュを更新してくれません。

いわば以下の図のような依存関係になっている様に思います。

keyの依存関係

もちろんurlが一致しておらずkeyを関連づけることができなくても、明示的にmutateAPIを使ってキャッシュを更新させることはできます。

https://swr.vercel.app/ja/docs/mutation

SWRの選定にはアプリケーションの設計がSWRの設計とマッチするかという観点でも検討が必要に感じました。

まとめ

今回はSWRとTanStack Queryを比較してきました。当然この記事の中では扱いきれないほどの違いがありますが、総じて言えることはSWRの方がシンプルに設計されていることです。それに対してTanStack Queryは多機能で様々な非同期の状態を扱うことができます。

一つの参考として、まずはSWRの導入を検討するのがいいかと思いました。しかし、SWRの設計がアプリケーションにマッチしなかったり、非同期処理における物足りなさを感じた場合にはTanStack Queryの採用を検討するのも良いでしょう。TanStack Queryはより柔軟なキャッシュの管理と非同期の状態を扱えます。

AI Shift Tech Blog

Discussion

kage1020kage1020

とても見通しのいい記事で参考になりました.
一点,グリッチのようなものとしてSWRに関して補足します.

SWRの場合はkeyに設定した値がfetcherに渡されます。
そのため、keyに設定するのはfetcherで使用するためのurlに限定されています。

keyfetcherに依存するのはその通りなのですが,逆に言えばfetcherを明示的にnullにすればkeyは任意になるのでURL以外も扱うことができます.

https://zenn.dev/tak_iwamoto/articles/39aefec675c323#swrを使用した状態管理

ytaisei(たいせー)ytaisei(たいせー)

コメントいただきありがとうございます!
SWRをGlobal Stateとして使用する方法もあるんですね。勉強になりました!

いただいた内容をもとに本記事にも追記させていただいております🙇‍♂️

paihupaihu

SWRの場合はkeyに設定した値がfetcherに渡されます。
そのため、keyに設定するのはfetcherで使用するためのurlに限定されています。

必ずしもfetcherの引数がurlでなければならないということはないので、
たとえば

const constructUrlForSomeFetcher = (id)=> `/api/data/${id}`

const someFetcher = ({id}) => fetch(constructUrlForSomeFetcher(id)).then(res => res.json()))

function App () {
  const { data, error } = useSWR({type: "someData", id: 10}, someFetcher)
  // ...
}

こういうこともできます。

keyurlが一致している必要はない。というところから、

これはつまりGET処理のエンドポイントと更新系処理のエンドポイントが一致している前提

このような前提もなくなります。

ytaisei(たいせー)ytaisei(たいせー)

コメントいただきありがとうございます!

必ずしもfetcherの引数がurlでなければならないということはないので、

確かに設計次第ではfetcherの引数がurlに限定されることはないですね。
大変勉強になりました🙇‍♂️