Reactのさまざまなデータフェッチ方法を比較して理解して正しく使用する - SWR・TanStack Query編
「Reactのさまざまなデータフェッチ方法を比較して理解して正しく使用する」シリーズの2記事目です。今回は「SWR・TanStack Queryを用いたデータフェッチ」について理解していきます。
- イントロ+useEffectを用いたデータフェッチ
- SWR・TanStack Queryを用いたデータフェッチ ← 👀この記事
- Pages Routerでのデータフェッチ+App Routerでのデータフェッチ+まとめ
Repository
以下は今シリーズで用いたリポジトリです。
🔽クライアントサイドフェッチの調査に用いたリポジトリ:React+Vite(useEffect, SWR・TanStack Query)
🔽サーバーサイドフェッチの調査に用いたリポジトリ:Next.js Pages Router, App RouterSWRを用いたクライアントサイドフェッチ
SWRを用いてデータのフェッチ・更新を行うときの挙動の確認から始めていきます。
まず、SWRのようなサードパーティ製のデータフェッチライブラリを使うことのメリットとしては次の点が挙げられます。
-
props
のバケツリレーを起こさずに、コンポーネント各々がオーナーシップを持ってデータを扱える点 - 各コンポーネントでデータフェッチを行うようにしても無駄なリクエストが発生しない点
- レスポンスのキャッシュが行える点
-
mutate
を使用して直感的に更新後の状態をUIに反映できる点 - データ取得中や更新中の状態管理をしやすいのでユーザに細かく正確なフィードバックを送ることができ、UXを高められる点
そのほかにもたくさんのメリットがSWRのドキュメントで紹介されています。
SWRを用いたデータフェッチの調査方法
それでは早速、SWRを用いてデータフェッチする処理を書いてみましょう。
useEffectを使ったデータフェッチに比べて、ここではデータ取得を行っておらず、各コンポーネントもprops
を持っていません。
その代わりに、データ取得のためのhooksをいくつか追加しました。
/src/prc-swr/hooks
これらのhooksをそのデータを必要とする各コンポーネントから呼び出してもらうことで、データ取得の責務を各コンポーネントが持つことができ、コンポーネント同士がprops
で密に接合された状態になることを防ぎます。
以下はuseSWR
を使用したデータフェッチのためのカスタムhooksの一例です。
useSWR()
はerror
やloading
, validating
(再検証中)などのデータ取得の際に起こる状態を返してくれるので、より細かで正確なフィードバックを行うことができます。
それでは、Personコンポーネントでユーザ名を更新してみましょう🏃🏻♀️POST
リクエストを送信しているだけです。
SWRではデータ更新の際にmutate
メソッドを使用することで同一のキーを持つリソースに対して再検証を発行 (データを期限切れとしてマークして再フェッチを発行) できます。
ここではmutate
メソッドがuseGetUser
内のuseSWR
から発行されたものですので、/api/get/user
をキーとして持つリソース、つまりuseGetUser
内部で使用しているuseSWR
に「そのデータ古いですよー」と伝えて再フェッチを促します。すると、validation
がトリガーされ、最新のデータがフェッチされてUIに反映されます。
結果
先ほどのデータ更新時の再レンダリング範囲に注目してみます。
以下のようにuseGetUser
を使用しているコンポーネントでのみ再レンダリングが発火していることが確認できるかと思います。
SWRを使うと限定的な範囲で再レンダリングができる
また、ほかにもSWRにはデータを最新に保つ仕組みがいくつか備わっています。その一部を見てみましょう。
Revalidate on Focus
windowにフォーカスが当たった場合に自動的に再検証が走り、最新のデータがフェッチされ、再レンダリングされます。
SWR: Revalidate on Focus
Revalidate on Interval
windowにフォーカスを当てずとも、ポーリング間隔を指定することで、一定の間隔でデータフェッチの問い合わせを行って再検証を走らせることができます。異なるデバイス間で定期的にデータ同期を行う際に便利です。
export const useGetUser = () => {
const url = "/api/get/user";
const { mutate, data, error, isLoading, isValidating } = useSWR(
`${import.meta.env.VITE_API_BASE_URL}${url}`,
fetcher,
+ {
+ refreshInterval: 5000,
+ }
);
return {
mutate,
user: data,
userError: error,
userIsLoading: isLoading,
userIsValidating: isValidating,
};
};
SWR: Revalidate on Interval
リクエストの重複
SWRにはリクエストの重複を排除する仕組みが備わっています。
この例では、各コンポーネントにデータ取得の責務を結びつけるために、内部でuseSWR
を用いたカスタムhooksをそれぞれの子コンポーネント内で呼び出していました。
これにより、ユーザ情報を必要とするPersonコンポーネントとHeaderコンポーネントそれぞれでuseGetUser
hookをコールすることになるのですが、リクエストが2回起こることにならないのでしょうか?😶
useSWR
では同じキーを持ち、ほぼ同時期にレンダリングされるコンポーネントに関しては、リクエストは一つにまとめられます。
ここでは
-
/api/user/get
@ Header, Personコンポーネント -
/api/get/unstale/data
@ Header, Contentコンポーネント -
https://github.com
@ Header, Contentコンポーネント
と、6回のAPIコールを実装していました。
SWRを使うと重複したリクエストは排除される
しかし、実際は3回のリクエストしか発生していません。
また、データ更新を行なったときも、更新後にrevalidationしたキーの紐づくデータの再フェッチしか行いません。SWRでは1度取得したレスポンスはクライアントサイドキャッシュに保存され、次に同じリクエストを送る場合はリクエストを送らずにキャッシュからデータが返される仕様になっているからです。
SWR は、まずキャッシュからデータを返し(stale)、次にフェッチリクエストを送り(revalidate)、最後に最新のデータを持ってくるという戦略です。
https://swr.vercel.app/ja
したがって、上でユーザ名を更新した時に起こるトランザクションは新しいuserPOST
とGET
の2回のみになり、githubやrandomNumberの再フェッチは行われません。
SWRを使うと再検証されるデータのみ再フェッチされ、あとはキャッシュから返される
この重複排除の仕組みのおかげで、リクエスト回数によるパフォーマンスの問題を気にせずにアプリ内でバシバシSWRフックを再利用することができます💪🏻❤️🔥
TanStack Queryを用いたクライアントサイドフェッチ
TanStack QueryもSWRと同様クライアントサイドキャッシュを利用したデータフェッチが行えるライブラリです。
バンドルサイズはSWRの3倍ほどありますが、Query Hooksの戻り値の種類が多かったり、Query Hooksが持っているoptionの数が多かったりとSWRよりも高機能です。
そんなTanStack Queryを用いてデータのフェッチ・更新を行うときの挙動も確認していきます。
TanStack Queryを用いたデータフェッチの調査
まずは、初期設定です。useContext
やuseEffect
などを使用しているため、TanStack Queryを使用するコンポーネントをまるっとQueryClientProvider
でラップします。
QueryClientProvider
はnew
したQueryClient
インスタンスと接続し、インスタンスを内部のコンポーネントに提供して使用できるようにしてくれます。
(ここでは一旦broadcastQueryClient
の存在は無視してください)
TanStack QueryでもSWRと同様、カスタムhooksを用いてデータ取得を各々のコンポーネントで行うため、props
のバケツリレーを防ぐことができます。
以下のように、データフェッチをカスタムhooksに切り出します。
こうすることでデータフェッチhooksが再利用可能になり、各コンポーネントでデータフェッチが行えるので、データ取得の責務をコンポーネントに委譲することができます。Personコンポーネントでユーザ名を更新してみます。useMutation
hooksが存在し、そのhooks
が更新・更新時の状態を管理します。
ここから少し細かい話に入ります。
useMutation
hooksに注目してほしいのですが、これが存在することにより、TanStack Queryではmutation
という処理を行っているときの状態が管理できます。つまり、mutate
が使われたとき=データ更新処理が発火したときからisPending
という状態を受け取ることができます。
データ更新が正常に行われると、onSuccess
でその状態を受け取り、queryClient.invalidateQueries({ queryKey: ["/api/get/user"] });
にて/api/get/user
をキーにもつリソースの再検証を発行します。
再検証を発行されたリソース(ここではTanStack QueryのuseGetUser
)はデータの再フェッチを行い、そのときの状態はuseGetUser
の内部で用いられているuseQuery
からisFetching
として受け取ることができます。
SWRとTanStack Queryでのmutateの比較
⭐️まとめると、TanStack QueryでuseMutationを用いたときのデータ更新処理は
-
mutate
でデータ更新をトリガーする - データ更新がトリガーされたら
isPending
を返す(Updating...表示) - データ更新が完了したら
onSuccess
が状態を受け取り、キーを持つリソースの再検証を発行する(useGetUser
にデータが古いことを伝えてリフェッチを促す)(Updating...非表示) -
useGetUser
が再検証を開始するとともにisFetching
を返す(⏳loading...表示) -
useGetUser
内のuseQuery
のqueryFn
の処理でデータの再フェッチを行う(⏳loading...表示) -
queryFn
の処理が完了する(⏳loading...非表示)
TanStack Queryでのデータ更新時の状態管理
⭐️SWRを用いたときのデータ更新処理は
-
fetch
関数が呼び出されてデータ更新が行われる - データ更新が完了する
-
mutate
(key)でキーを持つリソースの再検証を発行する(useGetUser
にデータが古いことを伝えてリフェッチを促す) -
useGetUser
が再検証を開始するとともにisValidating
を返す(⏳loading...表示) -
useGetUser
内のuseSWR
の第二引数の処理でデータの再フェッチを行う(⏳loading...表示) - 5の処理が完了する(⏳loading...非表示)
SWRでのデータ更新時の状態管理
となり、DB update処理中(API内部処理実行中)の状態を、TanStack Queryはwatchできるのに対し、SWRではその機能は提供されていないということになります。
(訂正)SWRメンテナーの方から、「SWR でも useSWRMutation
を使って mutation を行うと isMutating
として更新中の状態を取れるのでよかったら使ってみてください!」
というメッセージがありました。@koba04さん、ありがとうございます!
結果
少し脇道に逸れましたが、上記の動画より、TanStack QueryもSWRのようにuseGetUser
を使用しているコンポーネントでのみ再レンダリングが発火していることがわかります。TanStack Queryもキーによってデータの取得・更新処理を行うか否かを管理しているからです。
また、TanStack Queryにもデータを最新に保つ仕組みが備わっています。Window Focus RefetchingについてSWRと比較して見てみましょう。
Window Focus Refetching
v4まではwindowにフォーカスが当たった場合に自動的に再検証が走り、最新のデータに書きかわる、SWR同等の仕様でした。
しかし、こちらのPRによりfocus
イベントで再検証が走ることのデメリットが議論された結果、v5からはfocus
イベントではなくvisibilitychange
によって自動的再検証が走るような仕様になっているようです。
現状focusで再検証が走るSWR - devtoolから戻ってきたときや、windowがクリックされたとき、別ディスプレイに行って戻ってきたときにも再検証が走る
visibilitychangeで再検証が走るTanStack Query - 単にfocusでは再検証は走らない
focus
で再検証が走ることはSWRでも議論されており、PRも出ているので、将来的にはmergeされてTanStack Queryの仕様に近づくのだと思います。🏗️
リクエストの重複
こちらもSWR同様、リクエストをキーで管理しているので重複が排除されます。
TanStack Queryを使うと重複したリクエストは排除される
今回の記事ではSWRとTanStack Queryを使用したデータフェッチについて、理解を深めていきました。
SWRやTanStack Queryのサードパティー製のフェッチライブラリを使うことで、重複を排除したデータフェッチ、レスポンスのクライアントサイドキャッシュが行えるだけでなく、データ取得・更新時の状態を管理してくれるなどの自前で実装すると少し手の込む内部的なロジックを享受できます。
Reactのクライアントサイドでデータをフェッチする手段として最有力の候補として持っておきたいですね🌟
次回は、Reactのサーバーサイドでデータをフェッチする方法の代表としてNext.js Pages RouterでSSRのデータフェッチ・Next.js App RouterでReact Server Componentsを使用してのデータフェッチを理解していく記事です。
複雑なサーバーサイドでのデータフェッチについて、一緒に考えながら読んでいただけると幸いです✉️
(余談)TanStack QueryのbroadcastQueryClientという実験的な機能
TanStack Queryがwindowにフォーカスが当たった場合ではなくvisibilitychange
によってデータの再検証を行う方向になったお話を先ほどしました。
以前TanStack Queryを使用したときは、windowフォーカスで再検証が行われていたため、今回の調査の時にwindowを二つ開いて一つのwindowでデータを更新した時、もう一つのwindowに戻ってデータが更新されないことに(?)となり、Q&Aを投げてみました。
結果、私の確認不足ということで、v5
から上記の挙動に変わっていました。
しかし、今はbroadcastQueryClient
でアプリレベルでconnection
を張って変更を検知できるようにしている機能を開発してるよという回答をいただき、詳細な仕組みは理解できていませんが、それも試してみました。(実はこれもexperimentalとしてlatestのdocumentには明記されている)
"use client";
+import { broadcastQueryClient } from "@tanstack/query-broadcast-client-experimental";
import { QueryClient, QueryClientProvider } from "@tanstack/TanStack Query";
import BackButton from "../_component/back-button";
import LinkButton from "../_component/link-button";
import Content from "./children/content";
import Header from "./children/header";
import { Person } from "./children/user";
export default function TanstackPage() {
const queryClient = new QueryClient();
+ broadcastQueryClient({
+ queryClient,
+ broadcastChannel: "tanstack-app",
+ });
return (
<QueryClientProvider client={queryClient}>
<div>
<Header />
<Content />
<Person />
<BackButton />
<LinkButton link="/prc-swr" label="swr" />
<LinkButton link="/prc-fetch" label="fetch" />
<LinkButton link="/legacy-fetch" label="legacy" />
</div>
</QueryClientProvider>
);
}
片方のwindowで更新をかけると、同期的にもう片方のwindowでも値が変更される
動作環境:
"@tanstack/query-broadcast-client-experimental": "5.8.3"
"@tanstack/TanStack Query": "5.8.3"
Discussion