3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

クリスマスはサーバーステート管理ライブラリと過ごそう

Last updated at Posted at 2025-12-12

ジョブカン事業部のアドベントカレンダー13日目の記事です!

突然ですが、皆さんは恋人に何を求めますか?

いろんな答えがあると思うのですが、私個人的には「絶対に許せない欠点がないか」というのが重要なのではないかと思っています。

さて、それではライブラリを選ぶ話に移ります。ライブラリも完璧なものなどないので、どんなライブラリにも「良いところ」と「悪いところ」があります。ライブラリを選ぶときも、恋人を選ぶときと同じように、「自分にとって許せない弱点がないか?」をみて選んでみたらどうでしょうか?

この記事では、TanStack QueryとSWRそれぞれの良いところ・悪いところをまとめてみました。クリスマスにひとりぼっちのあなたも、気に入ったサーバーステート管理ライブラリと一緒に過ごすのはいかがでしょうか?

紹介するサーバーステート管理ライブラリの概要

この記事で紹介するTanStack Query(旧React Query)とSWRは、どちらもReactアプリケーションにおけるサーバーステート管理を効率化するためのライブラリです。

具体的には、今までuseEffectやuseStateを使ってデータを保持し、子孫コンポーネントにpropsで渡し…というコードを書いていたのを、useQuery/useSWRというフックを使うだけで自立分散的にサーバーからのデータ取得ができるようになります。

サーバーステート管理ライブラリがないとどうなるか

サーバーステート管理のライブラリが導入されておらず、カオスな状態管理が発生する場面は多々あるかと思います。

たとえば、以下のようなコンポーネント階層を想像してください。

PageコンポーネントでuserDataを取得し、それが必要なコンポーネントにpropsとして渡されています。User SummaryコンポーネントでuserDataを使うためには、Page → Header → User Summaryと2階層を経由してpropsを渡す必要があります。

この間のHeaderコンポーネントは、自分自身ではuserDataを使わないのに、子コンポーネントのためだけにpropsを受け取って渡しています。

このようなpropsのバケツリレーは、Reactではよくあるパターンです。管理するデータが2〜3個程度なら、それほど問題にはなりません。しかし、管理しなければいけないデータが10個、20個と増えていくとどうなるでしょうか?

  • Pageコンポーネントのstate管理が複雑化する
    どのstateがどのコンポーネントに影響するかを把握しづらくなる
  • 中間コンポーネント(HeaderやMain)のpropsが肥大化する
    自分では使わないpropsを大量に受け取ることになる
  • コンポーネントの再利用性が低下する
    → 特定のpropsを受け取ることが前提となり、他のコンテキストで使いづらくなる
  • データ取得ロジックが分散し、重複する可能性がある
    → それを防ぐにはデータ取得ロジックを配置する際に将来の拡張性も考慮する必要がある

結果として、メンテナンスコストが大きく上昇してしまいます。

サーバーステート管理ライブラリを使えば解決できる

一方で、サーバーステート管理ライブラリを使うと、各コンポーネントが必要なデータを自分で取得できるようになります。

先ほどの例で言えば、User SummaryコンポーネントとUser Detailsコンポーネントがそれぞれ独立してユーザーデータを取得できます。

これにより、以下のような利点があります。

  • 親コンポーネントを経由してpropsを渡す必要がなくなり、コードがシンプルになる
  • loading、error、dataといった状態も自動で管理してくれるため、ボイラープレートコードが大幅に削減される
  • 自立分散型の設計が可能になる
  • 同じデータを複数のコンポーネントで取得しようとしても、ライブラリが重複したfetchを防いでくれるため、パフォーマンスの心配もない

コードで比較してみる

具体的にコードで見てみましょう。

Before(従来の書き方)

// Page.tsx
// ❌ state管理が複雑化:データが増えるとここが肥大化する
// ❌ データ取得ロジックの分散:別の場所でも同じデータが必要になったら?
const Page: React.FC = () => {
  const [userData, setUserData] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const fetchUser = async () => {
      try {
        const response = await fetch('/api/user');
        const data = await response.json();
        setUserData(data);
      } catch (e) {
        setError(e as Error);
      } finally {
        setLoading(false);
      }
    };
    fetchUser();
  }, []);

  if (loading) return <div>Loading...</div>;
  if (error) return <div>Error: {error.message}</div>;

  return (
    <>
      <Header userData={userData} />
      <Main userData={userData} />
    </>
  );
};

// Header.tsx
// ❌ propsの肥大化:自分では使わないuserDataを受け取っている
// ❌ 再利用性の低下:userDataを渡す前提でしか使えない
const Header: React.FC<{ userData: User | null }> = ({ userData }) => {
  return (
    <header>
      <UserSummary userData={userData} />
    </header>
  );
};

// UserSummary.tsx
const UserSummary: React.FC<{ userData: User | null }> = ({ userData }) => {
  return (
    <div>
      <img src={userData?.profileImage} alt="profile" />
      <span>{userData?.name}</span>
    </div>
  );
};

After(TanStack Query)

// UserSummary.tsx
// ✅ 必要なコンポーネントで直接データ取得
// ✅ 別の場所で同じデータが必要になっても、同じqueryKeyを使えばキャッシュを共有
const UserSummary: React.FC = () => {
  const { data, isLoading, error } = useQuery({
    queryKey: ['user'],
    queryFn: () => fetch('/api/user').then(res => res.json()),
  });

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error?.message}</div>;

  return (
    <div>
      <img src={data?.profileImage} alt="profile" />
      <span>{data?.name}</span>
    </div>
  );
};

// Header.tsx
// ✅ propsが不要になりスッキリ
// ✅ 再利用性が向上:propsに依存せず、条件分岐があっても柔軟
const Header: React.FC = () => {
  return (
    <header>
      <UserSummary />
    </header>
  );
};

After(SWR)

// UserSummary.tsx
// ✅ 必要なコンポーネントで直接データ取得
// ✅ 別の場所で同じデータが必要になっても、同じキーを使えばキャッシュを共有
const UserSummary: React.FC = () => {
  const { data, isLoading, error } = useSWR('/api/user', fetcher);

  if (isLoading) return <div>Loading...</div>;
  if (error) return <div>Error: {error?.message}</div>;

  return (
    <div>
      <img src={data?.profileImage} alt="profile" />
      <span>{data?.name}</span>
    </div>
  );
};

Afterのコードでは、UserSummaryコンポーネントが自分で必要なデータを取得しています。
これにより、Headerコンポーネントはpropsを受け取る必要がなくなり、コードがシンプルになりました。

SWRの強み

軽量・シンプル

バンドルサイズが小さく、useSWRフック1つでほとんどのユースケースをカバーできます。

TanStack Queryは機能が豊富な分、queryClientinvalidateQueriesQueryClientProviderなど理解すべき概念が多く、学習コストが高くなりがちです。一方、SWRはシンプルなAPIで、学習コストを最小限に抑えられます。

フックの戻り値には、わかりやすく設計思想の違いが表れています。SWRはシンプルで学習しやすい反面、細かい状態管理が必要な場合は自分で実装する部分が増えます。

// SWR: シンプルな戻り値
const { data, error, isLoading, isValidating, mutate } = useSWR('/api/user', fetcher);

// 「初回ロード中」と「バックグラウンド更新中」を区別したい場合
const isInitialLoading = isLoading;  // data === undefined && isValidating
const isRefreshing = !isLoading && isValidating;  // 自分で判定
// TanStack Query: 細かい状態が最初から用意されている
const {
  data,
  error,
  isPending,      // 初回ロード中(dataがない状態)
  isFetching,     // 何らかのfetch中(初回・リフレッシュ両方)
  isRefetching,   // バックグラウンド更新中(dataがある状態でfetch中)
  isStale,        // キャッシュが古くなったか
  isSuccess,      // 成功したか
  isError,        // エラーか
  status,         // 'pending' | 'error' | 'success'
  fetchStatus,    // 'fetching' | 'paused' | 'idle'
  refetch,
} = useQuery({ queryKey: ['user'], queryFn: fetchUser });

このように、SWRは「シンプルだが自分で実装する部分が多い」、TanStack Queryは「学習コストは高いが標準機能でカバーできる範囲が広い」というトレードオフがあります。

バージョンアップの安定性

SWRは安定性を重視しており、バージョンアップによる破壊的変更が比較的少ないです。

v1.0(2021年)やv2.0(2022年)でも、デフォルトfetcherの削除や一部の型名変更など、影響範囲が限定的な変更が中心でした。

一方、TanStack Queryはメジャーバージョンアップ時に破壊的変更が入ることがあります。例えば、v4からv5へのマイグレーション(2023年11月)では以下のような変更がありました。

変更内容 詳細
引数の形式変更 useQuery(key, fn)useQuery({ queryKey, queryFn })
ステータス名変更 isLoadingisPendingisLoadingは別の意味に)
エラー型変更 デフォルトがunknownError
Hydration API変更 HydrateHydrationBoundaryにリネーム

公式でcodemodツールが提供されていますが、手動修正が必要な箇所も多いです。長期運用でバージョンアップに追従し続けるのが難しい場合は、SWRの方が適しています。

REST APIとの相性が良い

URLがそのままキャッシュキーになるため、直感的に使えます。

SWRConfigでfetcherをグローバルに設定すれば、各コンポーネントではキーを渡すだけで済みます。

// app.tsx - fetcherをグローバル設定
import { SWRConfig } from 'swr';

const fetcher = (url: string) => fetch(url).then(r => r.json());

const App: React.FC = () => (
  <SWRConfig value={{ fetcher }}>
    <MyApp />
  </SWRConfig>
);
// コンポーネントから使う - キーを渡すだけ
import useSWR from 'swr';

const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
  const { data, isLoading } = useSWR(`/api/users/${userId}`);
  // ...
};

一方、TanStack QueryではqueryOptionsを使ってクエリの設定を集約するパターンが推奨されますが、設定ファイルの作成が必要になります。

Next.jsとの親和性

同じVercel製であり、エコシステムとの統合がスムーズです。

  • Next.js公式ドキュメントでクライアントサイドのデータフェッチにSWRが推奨されている
  • Server Componentsで取得したデータをClient ComponentsでSWRに引き継ぐパターンが公式で紹介されている
// app/page.tsx (Server Component)
export default async function Page() {
  const data = await fetchData();  // サーバーで取得
  return <ClientComponent fallbackData={data} />;
}

// components/ClientComponent.tsx (Client Component)
'use client';
import useSWR from 'swr';

export function ClientComponent({ fallbackData }) {
  // サーバーで取得したデータを初期値として使用
  const { data } = useSWR('/api/data', fetcher, { fallbackData });
  return <div>{data.name}</div>;
}

TanStack Queryの強み

細かいキャッシュ制御

staleTimegcTimeなどでキャッシュの有効期限を細かく設定できます。

useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  staleTime: 1000 * 60 * 5,  // 5分間はfreshとみなす(再取得しない)
  gcTime: 1000 * 60 * 30,    // 30分後にキャッシュを削除
});

SWRのキャッシュ設定はdedupingInterval(重複排除の間隔)やrefreshInterval(定期的な再取得)で、キャッシュデータ自体をどうするかに関心があるのではなくリクエストをどうするかに関心があります。

// SWR: シンプルなキャッシュ設定
useSWR('/api/user', fetcher, {
  dedupingInterval: 2000,   // 2秒間は重複リクエストを排除
  refreshInterval: 30000,   // 30秒ごとに再取得
});

Mutation後に任意のキャッシュを無効化できる

queryClientを通じて、Reactコンポーネント外からもprefetch、invalidate、setQueryDataが可能です。

特にMutation後のキャッシュ無効化において、TanStack Queryは任意のキーを指定できます。

const queryClient = useQueryClient();

const { mutate } = useMutation({
  mutationFn: updateUser,
  onSuccess: () => {
    // 複数のキーを柔軟にinvalidate
    queryClient.invalidateQueries({ queryKey: ['user'] });
    queryClient.invalidateQueries({ queryKey: ['users'] });  // 一覧も更新
  },
});

一方、SWRのuseSWRMutationでは、同じキーを持つuseSWRのみが自動でrevalidateされます。

// SWR: 同じキーを持つuseSWRは自動でrevalidateされる
const { data } = useSWR('/api/user', fetcher);
const { trigger } = useSWRMutation('/api/user', updateUser);

// trigger実行後、'/api/user'のデータは自動的に再取得される
// しかし'/api/users'(一覧)は自動では更新されない
await trigger({ name: 'John' });
観点 SWR TanStack Query
手軽さ 自動で楽 毎回設定が必要
柔軟性 キーが一致する必要あり 任意のキーをinvalidate可能
制御 暗黙的 明示的

queryOptionsによる設定の集約

queryOptionsを使ってクエリの設定を一箇所に集約し、複数のコンポーネントやqueryClientから使い回すパターンが推奨されます。

// queries/user.ts - クエリ設定を集約
import { queryOptions } from '@tanstack/react-query';

export const userQueryOptions = (userId: string) =>
  queryOptions({
    queryKey: ['user', userId],
    queryFn: () => fetch(`/api/users/${userId}`).then(r => r.json()),
    staleTime: 1000 * 60 * 5,
  });

export const usersQueryOptions = queryOptions({
  queryKey: ['users'],
  queryFn: () => fetch('/api/users').then(r => r.json()),
});
// コンポーネントから使う
import { useQuery } from '@tanstack/react-query';
import { userQueryOptions } from './queries/user';

const UserProfile: React.FC<{ userId: string }> = ({ userId }) => {
  const { data, isPending } = useQuery(userQueryOptions(userId));
  // ...
};
// queryClientから使う(コンポーネント外でも可能)
import { usersQueryOptions } from './queries/user';

// prefetch
queryClient.prefetchQuery(usersQueryOptions);

// キャッシュの無効化
queryClient.invalidateQueries(usersQueryOptions);

// キャッシュの直接更新
queryClient.setQueryData(usersQueryOptions.queryKey, newData);

このパターンにより、クエリの設定が一箇所にまとまり、型安全性も保たれます。SWRでもSWRConfigでグローバル設定はできますが、クエリごとの設定を集約する仕組みはありません

条件付き実行が明示的

データ取得を条件付きで実行したい場合、enabledオプションで制御します。

const { data } = useQuery({
  queryKey: ['user', userId],
  queryFn: () => fetchUser(userId),
  enabled: !!userId,  // userIdがある時だけ実行
});

SWRではキーをnullにすると同様に条件付きで実行できますが…

こちらの方が直感的でわかりやすいと思います。

豊富な機能

無限スクロール、楽観的更新、並列クエリなどが標準APIとして整備されています。

例えば無限スクロールを比較すると、SWRのuseSWRInfiniteはシンプルですが、TanStack QueryのuseInfiniteQueryはより多くの機能を提供します。

// SWR: useSWRInfinite
const { data, size, setSize, isLoading } = useSWRInfinite(
  (index) => `/api/users?page=${index + 1}`,
  fetcher
);

const users = data ? data.flat() : [];
const hasMore = /* 自分で判定ロジックを実装 */;
const loadMore = () => setSize(size + 1);
// TanStack Query: useInfiniteQuery
const {
  data,
  fetchNextPage,
  hasNextPage,        // 次ページがあるか(自動判定)
  isFetchingNextPage, // 次ページ取得中か
} = useInfiniteQuery({
  queryKey: ['users'],
  queryFn: ({ pageParam }) => fetchUsers(pageParam),
  initialPageParam: 1,
  getNextPageParam: (lastPage) => lastPage.nextCursor,  // 次ページのパラメータを自動取得
});

const users = data?.pages.flat() ?? [];
// hasNextPage, fetchNextPageがそのまま使える
機能 SWR TanStack Query
次ページ有無の判定 自前実装 hasNextPageで自動
双方向スクロール 自前実装 fetchPreviousPageで標準対応
特定ページのみリフェッチ 不可 refetchPageオプションで可能

強力なDevTools

専用のGUIツールでキャッシュの状態を可視化しながらデバッグできます。キャッシュの中身、ステータス、リフェッチのタイミングを視覚的に確認できるため、開発効率が大幅に向上します。

SWRには公式のDevToolsはありませんが、コミュニティ製のSWRDevToolsが公式ドキュメントで紹介されており、ブラウザ拡張機能として利用できます。

公式から提供されている方がメンテナンスの面などで安心して利用しやすいかと思います。

Tanstack Routerとの統合

同じTanstackエコシステムのTanstack Routerと組み合わせると、ルート遷移前にデータをprefetchできます。

// routes/users.$userId.tsx
import { createFileRoute } from '@tanstack/react-router';
import { userQueryOptions } from '../queries/user';

export const Route = createFileRoute('/users/$userId')({
  // ルート遷移前にデータを取得
  loader: ({ context: { queryClient }, params: { userId } }) =>
    queryClient.ensureQueryData(userQueryOptions(userId)),
  component: UserPage,
});

function UserPage() {
  const { userId } = Route.useParams();
  // loaderで取得済みのデータを即座に表示
  const { data } = useSuspenseQuery(userQueryOptions(userId));
  return <div>{data.name}</div>;
}

queryOptionsで定義したクエリ設定をloaderとコンポーネントで共有でき、型安全にデータフェッチとルーティングを統合できます。

どちらを選ぶべきか

冒頭でお伝えした通り、大事なのは「許せない弱点がないか」です。
ここでは、それぞれの弱点が致命的になる状況を整理します。

SWRを避けるべき状況

以下に当てはまる場合、SWRの弱点が致命的になる可能性があります。

状況 なぜ致命的か
バックエンドがREST APIに従っていない キーとfetcherの引数が結合しているSWRの設計と相性が悪い
Mutation後に複数のキャッシュを更新したい 自動revalidateはキーが一致する場合のみ。手動対応が増える
Reactコンポーネント外からキャッシュを操作したい SWRはこのユースケースに弱い

TanStack Queryを避けるべき状況

以下に当てはまる場合、TanStack Queryの弱点が致命的になる可能性があります。

状況 なぜ致命的か
チームの学習コストを最小限に抑えたい 理解すべき概念が多く、導入・教育に時間がかかる
長期運用でバージョンアップに追従し続けるのが難しい 破壊的変更が多く、マイグレーション作業が発生しやすい
シンプルなREST APIとの連携だけで十分 高機能すぎてオーバースペックになる

どちらでも良い場合

上記のどちらにも強く当てはまらない場合は、チームの好みで選んで問題ありません。

  • 「とりあえず始めたい」 → SWR(シンプルで導入しやすい)
  • 「将来の拡張性を重視したい」 → TanStack Query(機能が豊富)

どちらを選んでも、サーバーステート管理ライブラリを導入すること自体が大きな改善になります。

私がどちらを選んだかというと…

私が趣味で開発しているプロジェクトでは、TanStack Queryを選びました。

理由としては、

  • 趣味レベルなので規模が小さいためバージョンアップへの追従が容易
  • TanStack Routerを使っていて、エコシステムの統一が図れる
  • 後から拡張性が欲しくなっても対応できるようにしたい

といった具合です。

実務でどちらを選ぶかは…かなり難しい問題ですが、私はTanStack Queryを推奨したいと思っています(笑)。

まとめ

観点 SWR TanStack Query
学習コスト 低い 高い
バンドルサイズ 小さい 大きい
標準機能の豊富さ シンプル 充実
バージョンアップの安定性 安定 破壊的変更あり
DevTools コミュニティ製あり 公式で強力
向いているケース シンプルなREST API連携、Next.js 複雑なキャッシュ制御、大規模アプリ

どちらを選んでも、中規模〜大規模のReactアプリケーションではサーバーステート管理ライブラリを導入すること自体が大きな改善になると思います。

この記事が、あなたにぴったりのサーバーステート管理ライブラリとの出会いの助けになれば幸いです。

公式ドキュメント

実際の導入手順や詳細な使い方については、各ライブラリの公式ドキュメントをご覧ください。


最後に

DONUTSでは新卒中途問わず積極的に採用活動を行っています。
ご興味あればぜひご覧ください。

3
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
3
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?