swr vs @tanstack/react-query 徹底比較

swr の詳細@tanstack/react-query の詳細
AI生成コンテンツ

この記事はAIによって生成されました。内容の正確性は保証されません。最新の情報は公式ドキュメントをご確認ください。

SWR vs TanStack Query(React Query)— どちらを選ぶべきか徹底比較

1. 結論

小〜中規模のプロジェクトでシンプルにデータフェッチを行いたいなら SWR中〜大規模で複雑なキャッシュ制御・ミューテーション・楽観的更新などを多用するなら TanStack Query を選ぶのがおすすめです。どちらも本番運用に十分な品質を持つライブラリであり、プロジェクトの要件と複雑度に応じて使い分けるのが最善です。


2. 比較表

観点SWRTanStack Query
開発元VercelTanner Linsley & コミュニティ
最新メジャーバージョン(2025年時点)v2.xv5.x
バンドルサイズ(minified + gzip)約 4.5 kB約 13 kB
TypeScript 対応✅ ファーストクラス✅ ファーストクラス
React Suspense 対応
SSR / Next.js 対応✅(Vercel 製で親和性高)✅(公式アダプタあり)
DevTools非公式のみ✅ 公式 DevTools あり
ミューテーション専用 API❌(mutate 関数で手動制御)useMutation
楽観的更新手動実装(可能だがボイラープレート多め)useMutationonMutate で公式サポート
無限スクロールuseSWRInfiniteuseInfiniteQuery
クエリの依存関係条件付きフェッチで実現enabled オプションで宣言的に制御
自動ガベージコレクションgcTime で制御可能
リトライ制御onErrorRetry で手動retry / retryDelay で宣言的
Polling(定期取得)refreshIntervalrefetchInterval
学習コスト⭐ 低い⭐⭐ やや高い
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 キャッシュの状態、クエリのステータス、再フェッチのタイミングなどをリアルタイムに可視化でき、デバッグ効率が格段に上がります。

  • 高度なキャッシュ制御 staleTimegcTime(旧 cacheTime)、queryKey の構造化により、きめ細かいキャッシュ戦略を宣言的に構築できます。

  • 楽観的更新の公式パターン onMutateonError(ロールバック)→ 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.isPendingmutation.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 の useSWRInfinitegetKey 関数で前ページの結果を受け取る関数型のアプローチです。TanStack Query の useInfiniteQuerygetNextPageParam / hasNextPage が組み込みで提供されるため、「次のページがあるか」の判定を自前で書く必要がありません。


5. どちらを選ぶべきか — ユースケース別の推奨

ユースケース推奨理由
読み取り中心の小規模アプリSWR軽量・シンプルで十分
Next.js App Router との統合SWRVercel 製で親和性が高い(ただし TanStack Query も問題なく動作)
バンドルサイズを極限まで削りたい