ブログ記事

SWR vs TanStack Query徹底比較 2025 - Reactデータフェッチライブラリの選び方

ReactアプリケーションのデータフェッチングとキャッシュManagementを劇的に改善するSWRとTanStack Queryを徹底比較。パフォーマンス、機能、使いやすさ、エコシステムの観点から、プロジェクトに最適な選択を解説します。

Web開発
React SWR TanStack Query データフェッチ キャッシュ
SWR vs TanStack Query徹底比較 2025 - Reactデータフェッチライブラリの選び方のヒーロー画像

2025 年の Reactアプリケーション開発において、効率的なデータフェッチングとキャッシュ管理は不可欠です。SWRTanStack Query(旧 React Query)は、この分野をリードする 2 つのライブラリです。本記事では、両者の特徴を徹底比較し、プロジェクトに最適な選択を支援します。

この記事で学べること

  • SWR と TanStack Query の基本概念と哲学の違い
  • パフォーマンス、バンドルサイズ、機能の詳細比較
  • 実践的な実装パターンとベストプラクティス
  • プロジェクトタイプ別の推奨選択基準
  • 移行戦略とエコシステムの活用方法

データフェッチングライブラリの必要性

手動でのステート管理

ローディング、エラー、データの管理が複雑

キャッシュの実装

効率的なキャッシュ戦略の実装が困難

リアルタイム更新

データの同期とバックグラウンド更新

専用ライブラリの登場

SWRとTanStack Queryが主流に

成熟したエコシステム

両ライブラリが業界標準として定着

SWR vs TanStack Query:基本比較

SWR vs TanStack Query 基本比較(2025年6月時点)
特徴 SWR TanStack Query 違い
開発元 Vercel(Next.js) Tanner Linsley 企業 vs 個人主導
初回リリース 2019年 2019年 同時期
バンドルサイズ ~13KB ~20KB SWRが軽量
依存関係 なし なし 両者ともゼロ依存
学習曲線 低い 中程度 SWRがシンプル
機能の豊富さ 必要十分 非常に豊富 TanStack Queryが高機能

コア哲学の違い

SWR: シンプルさと使いやすさ

SWR は”stale-while-revalidate”戦略に基づき、シンプルで直感的な API を提供します。

import useSWR from 'swr';

function Profile() {
  const { data, error, isLoading } = useSWR('/api/user', fetcher);
  
  if (error) return <div>エラーが発生しました</div>;
  if (isLoading) return <div>読み込み中...</div>;
  return <div>こんにちは、{data.name}さん!</div>;
}

TanStack Query: 高機能と柔軟性

TanStack Query は、より包括的なソリューションを提供し、複雑なユースケースに対応します。

import { useQuery } from '@tanstack/react-query';

function Profile() {
  const { data, error, isLoading } = useQuery({
    queryKey: ['user'],
    queryFn: fetchUser,
    staleTime: 5 * 60 * 1000, // 5分
    gcTime: 10 * 60 * 1000, // 10分(旧cacheTime)
  });
  
  if (error) return <div>エラー: {error.message}</div>;
  if (isLoading) return <div>読み込み中...</div>;
  return <div>こんにちは、{data.name}さん!</div>;
}

機能比較

基本的なデータフェッチング

// SWRの基本的な使い方 const fetcher = url => fetch(url).then(r => r.json()); function useUser(id) { const { data, error, isLoading, mutate } = useSWR( id ? `/api/users/${id}` : null, fetcher, { revalidateOnFocus: true, revalidateOnReconnect: true, refreshInterval: 0, } ); return { user: data, isLoading, isError: error, mutate }; }
// TanStack Queryの基本的な使い方 function useUser(id) { const { data, error, isLoading, refetch } = useQuery({ queryKey: ['users', id], queryFn: () => fetchUser(id), enabled: !!id, refetchOnWindowFocus: true, refetchOnReconnect: true, refetchInterval: false, }); return { user: data, isLoading, isError: !!error, refetch }; }
SWR
// SWRの基本的な使い方 const fetcher = url => fetch(url).then(r => r.json()); function useUser(id) { const { data, error, isLoading, mutate } = useSWR( id ? `/api/users/${id}` : null, fetcher, { revalidateOnFocus: true, revalidateOnReconnect: true, refreshInterval: 0, } ); return { user: data, isLoading, isError: error, mutate }; }
TanStack Query
// TanStack Queryの基本的な使い方 function useUser(id) { const { data, error, isLoading, refetch } = useQuery({ queryKey: ['users', id], queryFn: () => fetchUser(id), enabled: !!id, refetchOnWindowFocus: true, refetchOnReconnect: true, refetchInterval: false, }); return { user: data, isLoading, isError: !!error, refetch }; }

パフォーマンス最適化機能

キャッシュ戦略の比較

チャートを読み込み中...

高度な機能比較

1. ミューテーション(データ更新)

// SWRのミューテーション
import useSWRMutation from 'swr/mutation';

function UpdateProfile() {
  const { trigger, isMutating } = useSWRMutation(
    '/api/user',
    async (url, { arg }) => {
      const res = await fetch(url, {
        method: 'PUT',
        body: JSON.stringify(arg)
      });
      return res.json();
    }
  );
  
  const handleUpdate = async (data) => {
    try {
      await trigger(data);
      // 成功時の処理
    } catch (e) {
      // エラー処理
    }
  };
  
  return (
    <button onClick={() => handleUpdate({ name: '新しい名前' })}>
      更新 {isMutating && '中...'}
    </button>
  );
}
// TanStack Queryのミューテーション
import { useMutation, useQueryClient } from '@tanstack/react-query';

function UpdateProfile() {
  const queryClient = useQueryClient();
  
  const mutation = useMutation({
    mutationFn: (data) => {
      return fetch('/api/user', {
        method: 'PUT',
        body: JSON.stringify(data)
      }).then(r => r.json());
    },
    onSuccess: (data) => {
      // キャッシュの更新
      queryClient.setQueryData(['user'], data);
      // または再フェッチ
      queryClient.invalidateQueries({ queryKey: ['user'] });
    },
    onError: (error) => {
      console.error('更新エラー:', error);
    }
  });
  
  return (
    <button onClick={() => mutation.mutate({ name: '新しい名前' })}>
      更新 {mutation.isPending && '中...'}
    </button>
  );
}

SWRの特徴:

  • シンプルな API
  • 自動的な楽観的更新
  • mutate 関数での直接的なキャッシュ操作

TanStack Queryの特徴:

  • より詳細な制御
  • onSuccessonError などのライフサイクルフック
  • キャッシュ無効化の細かい制御

使い分け:

  • シンプルな更新: SWR
  • 複雑な更新フロー: TanStack Query

2. 無限スクロール・ページネーション

import useSWRInfinite from 'swr/infinite'; function InfiniteList() { const getKey = (pageIndex, previousPageData) => { if (previousPageData && !previousPageData.length) return null; return `/api/posts?page=${pageIndex}`; }; const { data, error, size, setSize, isValidating } = useSWRInfinite(getKey, fetcher); const posts = data ? data.flat() : []; const isLoadingMore = isValidating && size > 0 && data && typeof data[size - 1] === 'undefined'; const isEmpty = data?.[0]?.length === 0; const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 10); return ( <div> {posts.map(post => <PostItem key={post.id} {...post} />)} <button disabled={isLoadingMore || isReachingEnd} onClick={() => setSize(size + 1)} > {isLoadingMore ? '読み込み中...' : isReachingEnd ? '全て表示しました' : 'もっと見る'} </button> </div> ); }
import { useInfiniteQuery } from '@tanstack/react-query'; function InfiniteList() { const { data, error, fetchNextPage, hasNextPage, isFetchingNextPage, status, } = useInfiniteQuery({ queryKey: ['posts'], queryFn: ({ pageParam }) => fetchPosts(pageParam), initialPageParam: 0, getNextPageParam: (lastPage, pages) => { return lastPage.hasMore ? pages.length : undefined; }, }); return ( <div> {data?.pages.map((page, i) => ( <React.Fragment key={i}> {page.posts.map(post => <PostItem key={post.id} {...post} /> )} </React.Fragment> ))} <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? '読み込み中...' : hasNextPage ? 'もっと見る' : '全て表示しました'} </button> </div> ); }
SWR Infinite
import useSWRInfinite from 'swr/infinite'; function InfiniteList() { const getKey = (pageIndex, previousPageData) => { if (previousPageData && !previousPageData.length) return null; return `/api/posts?page=${pageIndex}`; }; const { data, error, size, setSize, isValidating } = useSWRInfinite(getKey, fetcher); const posts = data ? data.flat() : []; const isLoadingMore = isValidating && size > 0 && data && typeof data[size - 1] === 'undefined'; const isEmpty = data?.[0]?.length === 0; const isReachingEnd = isEmpty || (data && data[data.length - 1]?.length < 10); return ( <div> {posts.map(post => <PostItem key={post.id} {...post} />)} <button disabled={isLoadingMore || isReachingEnd} onClick={() => setSize(size + 1)} > {isLoadingMore ? '読み込み中...' : isReachingEnd ? '全て表示しました' : 'もっと見る'} </button> </div> ); }
TanStack Query Infinite
import { useInfiniteQuery } from '@tanstack/react-query'; function InfiniteList() { const { data, error, fetchNextPage, hasNextPage, isFetchingNextPage, status, } = useInfiniteQuery({ queryKey: ['posts'], queryFn: ({ pageParam }) => fetchPosts(pageParam), initialPageParam: 0, getNextPageParam: (lastPage, pages) => { return lastPage.hasMore ? pages.length : undefined; }, }); return ( <div> {data?.pages.map((page, i) => ( <React.Fragment key={i}> {page.posts.map(post => <PostItem key={post.id} {...post} /> )} </React.Fragment> ))} <button onClick={() => fetchNextPage()} disabled={!hasNextPage || isFetchingNextPage} > {isFetchingNextPage ? '読み込み中...' : hasNextPage ? 'もっと見る' : '全て表示しました'} </button> </div> ); }

3. リアルタイム更新

// SWRでのリアルタイム更新
function LiveData() {
  const { data, mutate } = useSWR('/api/live-data', fetcher, {
    refreshInterval: 1000, // 1秒ごとにポーリング
  });
  
  // WebSocketとの統合
  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com/live');
    
    ws.onmessage = (event) => {
      const newData = JSON.parse(event.data);
      mutate(newData, false); // キャッシュを更新(再検証なし)
    };
    
    return () => ws.close();
  }, [mutate]);
  
  return <div>{data?.value}</div>;
}

// TanStack Queryでのリアルタイム更新
function LiveData() {
  const queryClient = useQueryClient();
  const { data } = useQuery({
    queryKey: ['live-data'],
    queryFn: fetchLiveData,
    refetchInterval: 1000, // 1秒ごとにポーリング
  });
  
  // WebSocketとの統合
  useEffect(() => {
    const ws = new WebSocket('wss://api.example.com/live');
    
    ws.onmessage = (event) => {
      const newData = JSON.parse(event.data);
      queryClient.setQueryData(['live-data'], newData);
    };
    
    return () => ws.close();
  }, [queryClient]);
  
  return <div>{data?.value}</div>;
}

エコシステムと統合

フレームワークサポート

フレームワークサポート比較
フレームワーク SWR TanStack Query 備考
Next.js ◎ ネイティブ ○ 良好 SWRはVercel製
Remix ○ 良好 ◎ 推奨 Remixドキュメントで推奨
Gatsby ○ 良好 ○ 良好 両方とも問題なし
React Native ○ 良好 ◎ 優秀 TanStack Queryが充実
Vue/Svelte ✗ 非対応 ◎ 対応 TanStackは多フレームワーク対応

DevToolsとデバッグ

SWR DevTools 70 %
TanStack Query DevTools 95 %

TanStack Query DevTools は、より包括的なデバッグ体験を提供します:

// TanStack Query DevToolsの設定
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <YourApp />
      <ReactQueryDevtools initialIsOpen={false} />
    </QueryClientProvider>
  );
}

パフォーマンス最適化

バンドルサイズの最適化

// SWR - 必要な機能のみインポート
import useSWR from 'swr';
import useSWRMutation from 'swr/mutation';
import useSWRInfinite from 'swr/infinite';

// TanStack Query - モジュラーインポート
import { useQuery } from '@tanstack/react-query';
import { useMutation } from '@tanstack/react-query';
import { useInfiniteQuery } from '@tanstack/react-query';

キャッシュ戦略の最適化

// SWRのキャッシュ戦略
const { data } = useSWR(key, fetcher, {
  // キャッシュの設定
  dedupingInterval: 2000, // 2秒間は重複リクエストを防ぐ
  focusThrottleInterval: 5000, // フォーカス時の再検証を5秒に1回に制限
  
  // 再検証の設定
  revalidateIfStale: true,
  revalidateOnMount: true,
  revalidateOnFocus: true,
  revalidateOnReconnect: true,
  
  // エラーハンドリング
  shouldRetryOnError: true,
  errorRetryInterval: 5000,
  errorRetryCount: 3,
  
  // パフォーマンス
  suspense: false,
  keepPreviousData: true,
});

// グローバル設定
import { SWRConfig } from 'swr';

<SWRConfig value={{
  refreshInterval: 3000,
  fetcher: (url) => fetch(url).then(res => res.json()),
  onError: (error, key) => {
    console.error(`SWRエラー ${key}:`, error);
  }
}}>
  <App />
</SWRConfig>
// TanStack Queryのキャッシュ戦略
const { data } = useQuery({
  queryKey: ['todos', filters],
  queryFn: fetchTodos,
  
  // キャッシュの設定
  staleTime: 5 * 60 * 1000, // 5分間は新鮮とみなす
  gcTime: 10 * 60 * 1000, // 10分後にガベージコレクション
  
  // 再フェッチの設定
  refetchOnMount: true,
  refetchOnWindowFocus: true,
  refetchOnReconnect: true,
  refetchInterval: false,
  
  // エラーハンドリング
  retry: 3,
  retryDelay: attemptIndex => Math.min(1000 * 2 ** attemptIndex, 30000),
  
  // パフォーマンス
  structuralSharing: true,
  networkMode: 'offlineFirst',
});

// グローバル設定
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,
      gcTime: 10 * 60 * 1000,
      retry: 3,
    },
  },
});

共通のベストプラクティス:

  1. 適切なキャッシュ時間の設定

    • データの更新頻度に応じて調整
    • ユーザー体験とサーバー負荷のバランス
  2. エラーハンドリング

    • 再試行回数の制限
    • エクスポネンシャルバックオフ
  3. ネットワーク最適化

    • 重複リクエストの防止
    • バックグラウンド更新の活用
  4. メモリ管理

    • 不要なキャッシュの削除
    • 大量データの適切な処理

プロジェクト別の選択基準

選択フローチャート

チャートを読み込み中...

推奨される使用ケース

SWRが最適な場合:

  • Next.jsプロジェクト
  • シンプルなデータフェッチング要件
  • 軽量なバンドルサイズが重要
  • 学習コストを最小限にしたい
  • Vercelエコシステムを活用

TanStack Queryが最適な場合:

  • 複雑なキャッシュ戦略が必要
  • 複数のフレームワークで使用
  • 高度な DevTools が必要
  • オフラインファーストアプリ
  • エンタープライズレベルの要件

移行戦略

SWRからTanStack Queryへの移行

// 移行前(SWR)
const { data, error, mutate } = useSWR('/api/user', fetcher);

// 移行後(TanStack Query)
const { data, error, refetch } = useQuery({
  queryKey: ['user'],
  queryFn: () => fetcher('/api/user'),
});

// 移行ヘルパー関数
function useSWRCompat(key, fetcher, options = {}) {
  const query = useQuery({
    queryKey: Array.isArray(key) ? key : [key],
    queryFn: () => fetcher(key),
    refetchInterval: options.refreshInterval,
    enabled: key !== null,
  });
  
  return {
    data: query.data,
    error: query.error,
    isLoading: query.isLoading,
    mutate: query.refetch,
  };
}

TanStack Query は単なるデータフェッチングライブラリではありません。 非同期状態管理の完全なソリューションです。

Tanner Linsley TanStack Query作者

まとめ

SWR と TanStack Query は、どちらも優れたデータフェッチングライブラリです。選択の際は以下を考慮してください:

選択のポイント

  • プロジェクトの規模と複雑性
  • チームの技術スタックと経験
  • 必要な機能の範囲
  • パフォーマンス要件
  • 将来の拡張性

最終的に、両ライブラリとも本番環境で実績があり、活発にメンテナンスされています。プロジェクトの要件に最も適したものを選択することが成功への鍵となります。

この記事は役に立ちましたか?

Daily Hackでは、開発者の皆様に役立つ情報を毎日発信しています。