SWR vs TanStack Query(React Query)— どちらを選ぶべきか徹底比較
1. 結論
小〜中規模のプロジェクトでシンプルにデータフェッチを行いたいなら SWR、中〜大規模で複雑なキャッシュ制御・ミューテーション・楽観的更新などを多用するなら TanStack Query を選ぶのがおすすめです。どちらも本番運用に十分な品質を持つライブラリであり、プロジェクトの要件と複雑度に応じて使い分けるのが最善です。
2. 比較表
| 観点 | SWR | TanStack Query |
|---|---|---|
| 開発元 | Vercel | Tanner Linsley & コミュニティ |
| 最新メジャーバージョン(2025年時点) | v2.x | v5.x |
| バンドルサイズ(minified + gzip) | 約 4.5 kB | 約 13 kB |
| TypeScript 対応 | ✅ ファーストクラス | ✅ ファーストクラス |
| React Suspense 対応 | ✅ | ✅ |
| SSR / Next.js 対応 | ✅(Vercel 製で親和性高) | ✅(公式アダプタあり) |
| DevTools | 非公式のみ | ✅ 公式 DevTools あり |
| ミューテーション専用 API | ❌(mutate 関数で手動制御) | ✅ useMutation |
| 楽観的更新 | 手動実装(可能だがボイラープレート多め) | useMutation の onMutate で公式サポート |
| 無限スクロール | useSWRInfinite | useInfiniteQuery |
| クエリの依存関係 | 条件付きフェッチで実現 | enabled オプションで宣言的に制御 |
| 自動ガベージコレクション | ❌ | ✅ gcTime で制御可能 |
| リトライ制御 | onErrorRetry で手動 | retry / retryDelay で宣言的 |
| Polling(定期取得) | refreshInterval | refetchInterval |
| 学習コスト | ⭐ 低い | ⭐⭐ やや高い |
| GitHub Stars(参考) | 約 30k | 約 43k |
| 週間ダウンロード数 | 約 300万 | 約 700万 |
3. それぞれの強み
SWR の強み
-
圧倒的な軽量さ バンドルサイズが約 4.5 kB と非常に小さく、パフォーマンスバジェットに厳しいプロジェクトに最適です。
-
API がシンプル
useSWR(key, fetcher)という最小限のインターフェースで、学習コストがほぼゼロに近いです。 -
Next.js との親和性 同じ Vercel が開発しているため、Next.js の App Router / Pages Router いずれとも自然に統合できます。
-
Stale-While-Revalidate 戦略の忠実な実装 HTTP の
stale-while-revalidateキャッシュ戦略をそのまま React Hooks に落とし込んだ設計思想が明快です。
TanStack Query の強み
-
ミューテーションの一級サポート
useMutationフックにより、POST/PUT/DELETE 操作のローディング・エラー・成功状態を宣言的に管理できます。 -
公式 DevTools キャッシュの状態、クエリのステータス、再フェッチのタイミングなどをリアルタイムに可視化でき、デバッグ効率が格段に上がります。
-
高度なキャッシュ制御
staleTime、gcTime(旧cacheTime)、queryKeyの構造化により、きめ細かいキャッシュ戦略を宣言的に構築できます。 -
楽観的更新の公式パターン
onMutate→onError(ロールバック)→onSettled(再検証)という一連のフローが API レベルで整備されています。 -
フレームワーク非依存の設計
@tanstack/query-coreをベースに React / Vue / Solid / Svelte / Angular 向けアダプタが提供されており、マルチフレームワーク対応が可能です。
4. コード例で比較
4-1. 基本的なデータフェッチ
SWR
import useSWR from "swr";
interface User {
id: number;
name: string;
email: string;
}
const fetcher = (url: string): Promise<User> =>
fetch(url).then((res) => {
if (!res.ok) throw new Error("Fetch failed");
return res.json();
});
function UserProfile({ userId }: { userId: number }) {
const { data, error, isLoading } = useSWR<User>(
`/api/users/${userId}`,
fetcher
);
if (isLoading) return <p>読み込み中...</p>;
if (error) return <p>エラーが発生しました</p>;
return (
<div>
<h2>{data?.name}</h2>
<p>{data?.email}</p>
</div>
);
}
TanStack Query
import { useQuery } from "@tanstack/react-query";
interface User {
id: number;
name: string;
email: string;
}
const fetchUser = async (userId: number): Promise<User> => {
const res = await fetch(`/api/users/${userId}`);
if (!res.ok) throw new Error("Fetch failed");
return res.json();
};
function UserProfile({ userId }: { userId: number }) {
const { data, error, isLoading } = useQuery<User>({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
});
if (isLoading) return <p>読み込み中...</p>;
if (error) return <p>エラーが発生しました</p>;
return (
<div>
<h2>{data?.name}</h2>
<p>{data?.email}</p>
</div>
);
}
ポイント: 基本的な読み取りだけであれば、両者のコード量・複雑度にほとんど差はありません。SWR はキーがそのまま fetcher の引数になる点がシンプルで、TanStack Query は
queryKeyを構造化配列で管理する点が特徴です。
4-2. ミューテーション(データ更新)
SWR
import useSWR, { useSWRConfig } from "swr";
function UpdateUserName({ userId }: { userId: number }) {
const { mutate } = useSWRConfig();
const { data: user } = useSWR<User>(`/api/users/${userId}`, fetcher);
const handleUpdate = async () => {
const newName = "新しい名前";
// 楽観的更新: キャッシュを即座に書き換える
mutate(
`/api/users/${userId}`,
{ ...user!, name: newName },
false // 再検証を一旦スキップ
);
try {
await fetch(`/api/users/${userId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: newName }),
});
// 成功後にサーバーデータで再検証
mutate(`/api/users/${userId}`);
} catch {
// 失敗時はキャッシュを元に戻して再検証
mutate(`/api/users/${userId}`);
}
};
return <button onClick={handleUpdate}>名前を更新</button>;
}
TanStack Query
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
function UpdateUserName({ userId }: { userId: number }) {
const queryClient = useQueryClient();
const { data: user } = useQuery<User>({
queryKey: ["user", userId],
queryFn: () => fetchUser(userId),
});
const mutation = useMutation({
mutationFn: (newName: string) =>
fetch(`/api/users/${userId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ name: newName }),
}).then((res) => {
if (!res.ok) throw new Error("Update failed");
return res.json();
}),
onMutate: async (newName) => {
// 進行中のフェッチをキャンセル
await queryClient.cancelQueries({ queryKey: ["user", userId] });
// 現在のキャッシュを退避(ロールバック用)
const previousUser = queryClient.getQueryData<User>(["user", userId]);
// 楽観的にキャッシュを更新
queryClient.setQueryData<User>(["user", userId], (old) =>
old ? { ...old, name: newName } : old
);
return { previousUser };
},
onError: (_err, _newName, context) => {
// エラー時にロールバック
if (context?.previousUser) {
queryClient.setQueryData(["user", userId], context.previousUser);
}
},
onSettled: () => {
// 成功・失敗問わず再検証
queryClient.invalidateQueries({ queryKey: ["user", userId] });
},
});
return (
<div>
<button
onClick={() => mutation.mutate("新しい名前")}
disabled={mutation.isPending}
>
{mutation.isPending ? "更新中..." : "名前を更新"}
</button>
{mutation.isError && <p>更新に失敗しました</p>}
</div>
);
}
ポイント: SWR では楽観的更新のロールバックを自前で管理する必要がありますが、TanStack Query は
onMutate/onError/onSettledのライフサイクルが整備されており、ロールバック用のコンテキストを型安全に受け渡せます。またmutation.isPendingやmutation.isErrorで UI 状態を宣言的に制御できる点が大きな違いです。
4-3. 無限スクロール
SWR
import useSWRInfinite from "swr/infinite";
interface Page {
data: User[];
nextCursor: number | null;
}
function UserList() {
const getKey = (pageIndex: number, previousPageData: Page | null) => {
if (previousPageData && !previousPageData.nextCursor) return null;
if (pageIndex === 0) return "/api/users?cursor=0";
return `/api/users?cursor=${previousPageData!.nextCursor}`;
};
const { data, size, setSize, isLoading } = useSWRInfinite<Page>(
getKey,
(url: string) => fetch(url).then((r) => r.json())
);
const users = data?.flatMap((page) => page.data) ?? [];
const isReachingEnd = data?.[data.length - 1]?.nextCursor === null;
return (
<div>
{users.map((user) => (
<p key={user.id}>{user.name}</p>
))}
<button
onClick={() => setSize(size + 1)}
disabled={isLoading || isReachingEnd}
>
もっと読み込む
</button>
</div>
);
}
TanStack Query
import { useInfiniteQuery } from "@tanstack/react-query";
interface Page {
data: User[];
nextCursor: number | null;
}
function UserList() {
const {
data,
fetchNextPage,
hasNextPage,
isFetchingNextPage,
isLoading,
} = useInfiniteQuery<Page>({
queryKey: ["users"],
queryFn: ({ pageParam }) =>
fetch(`/api/users?cursor=${pageParam}`).then((r) => r.json()),
initialPageParam: 0,
getNextPageParam: (lastPage) => lastPage.nextCursor ?? undefined,
});
const users = data?.pages.flatMap((page) => page.data) ?? [];
return (
<div>
{users.map((user) => (
<p key={user.id}>{user.name}</p>
))}
<button
onClick={() => fetchNextPage()}
disabled={isLoading || isFetchingNextPage || !hasNextPage}
>
もっと読み込む
</button>
</div>
);
}
ポイント: SWR の
useSWRInfiniteはgetKey関数で前ページの結果を受け取る関数型のアプローチです。TanStack Query のuseInfiniteQueryはgetNextPageParam/hasNextPageが組み込みで提供されるため、「次のページがあるか」の判定を自前で書く必要がありません。
5. どちらを選ぶべきか — ユースケース別の推奨
| ユースケース | 推奨 | 理由 |
|---|---|---|
| 読み取り中心の小規模アプリ | SWR | 軽量・シンプルで十分 |
| Next.js App Router との統合 | SWR | Vercel 製で親和性が高い(ただし TanStack Query も問題なく動作) |
| バンドルサイズを極限まで削りたい |