Reactのエフェクト(Effect)の視点で考える Tanstack Query 〜Reactの思想にもとづく使い分けの一例〜

エフェクト(Effect)という視点から、Tanstack Query の useQuery と useMutation を紐解きます。レンダリングとユーザーインタラクションに応じた、React らしい非同期処理の実装を考察します。

katsutaApril 1, 2025

Tanstack Query(旧 React Query)は、サーバーデータの取得・キャッシュ・同期を効率よく管理するためのライブラリです。柔軟なキャッシュ管理機能などを備えており、効率的な非同期処理の状態管理を実現します。
そのため、数多くの開発者から絶大な支持を得ています。

Tanstack Query が提供する API の中でも、useQueryuseMutation は代表的なものであり、通常は次のように使い分けられるかと思います。

  • useQuery:サーバーデータの取得およびキャッシュ管理を行う React フック
  • useMutation:データの作成・更新・削除などの操作を行う React フック

一般的に、データ取得(GET)処理には useQuery を用いることを考えるかもしれません。
しかし、重要なのは「その処理は何を起点として実行されるべきか?」という視点です。
レンダリングに伴って自動的に実行するのか、それともユーザーの操作によって実行されるべきなのか、この違いを意識することで適切な使い分けが見えてきます。

本記事では、データ取得処理に焦点を当て、「React のエフェクト」と「イベントハンドラ」の違いを軸にその使い分けを考察していきます。

Reactのエフェクトとしての性質

まず、 useQuery が React のエフェクトとしての性質を持つと考えてみましょう。
React 公式ドキュメントでは、エフェクトを次のように説明しています:

エフェクトは、特定のイベントによってではなく、レンダー自体によって引き起こされる副作用を指定するためのものです。 参照: https://ja.react.dev/learn/synchronizing-with-effects#what-are-effects-and-how-are-they-different-from-events

useQuery は、コンポーネントのレンダーがトリガーとなり、外部データと同期を図ります。
この動作は、まさに「レンダー自体によって引き起こされる副作用」に該当すると考えることができます。

また、エフェクトとイベントハンドラの違いについても次のように説明されています:

あるコードがエフェクトにあるべきか、イベントハンドラにあるべきかわからない場合は、そのコードが実行される理由を自問してください。コンポーネントがユーザに表示されたために実行されるべきコードにのみエフェクトを使用してください。 参照: https://ja.react.dev/learn/you-might-not-need-an-effect#sharing-logic-between-event-handlers

これに基づくと、useQuery は「コンポーネントが表示されたときに実行されるべきコード」に該当します。

一方で「ユーザがボタンを押したときにデータを取得する」という、特定のユーザインタラクションが起因となる処理はイベントハンドラとして扱うべきであり、それを実現するための API として useMutation が適していると筆者は考えます。

以下は、ボタン押下時にサーバーからデータを取得し、表示するケースを考えた実装例です。

const useTodo = () => {
  return useQuery({
    queryKey: ["todo"],
    queryFn: fetcher,
    enabled: false,
  });
};
 
export const Component = () => {
  const { refetch, isFetching, data } = useTodo();
  const handleClick = () => refetch();
 
  return (
    <div>
      <button onClick={handleClick}>取得!</button>
      <div>
        {isFetching ? (
          <p>loading...</p>
        ) : (
          <>
            {data?.map((d) => (
              <div key={d.id}>
                {d.id}: {d.title}
              </div>
            ))}
          </>
        )}
      </div>
    </div>
  );
};

注目するべきはオプションに enabled を指定していることです。 enabled オプションを false に設定すると、useQuery はコンポーネントのレンダー時に自動でデータ取得を実行しなくなります。代わりに、refetch を実行することでデータ取得を制御できます。(よく見るパターンですね。)

この方法は「レンダー時に自動的に起動するはずの処理(エフェクト)を止めておき、手動で制御する」という構造になっています。これは本来のエフェクトの役割である「コンポーネントと外部データの同期」という性質とは少し異なり、始めから同期自体を停止し、イベントで制御するという、やや矛盾した実装になっていると考えることもできます。
処理の意図が「ユーザーの明示的なアクションに応じてデータを取得する」であるなら、その意図を素直に表現できる useMutation の方が React らしい実装であると主張します。
以下に例を示します。

const useTodo = () => {
  return useMutation({
    mutationFn: fetcher,
  });
};
 
export const Component = () => {
  const { data, isPending, mutate } = useTodo();
  const handleClick = () => mutate();
 
  return (
    <div>
      <button onClick={handleClick}>取得!</button>
      ...

useMutation は本来、「データの更新操作(POST / PUT / DELETE など)」に使うイメージが強いかもしれません。
しかし、「処理の起点」に着目すると「ユーザーアクションに応じて実行する」という意図を直接表現できる useMutation のほうが、React の設計思想に合致するのではと考えます。

まとめ

本記事で紹介した例は単純なものです。データの再利用性、自動再検証、キャッシュ戦略などを考慮した上で、アプリケーションに適した選択をすることをお勧めします。
ただ、最初に考えるべきなのは、その処理が何が起因となり実行されるのかという点であり、ライブラリが提供する API を扱う際も常にこの視点を忘れずにいたいですね。

もしご意見や改善点がありましたら、ぜひご指摘ください。