😶‍🌫️

エフェクト(Effect)の視点で考える useQuery と useMutation

2025/01/02に公開

非同期処理の状態管理として絶大な支持を得ているTanstack Query
この記事では、ユーザインタラクションによるデータ取得処理におけるuseQueryuseMutationの使い分けについて考察します。

まず、 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は「コンポーネントが表示されたときに実行されるべきコード」に該当します。一方で、「ユーザがボタンを押したときにデータを取得する」といった、特定のユーザインタラクションがトリガーとなる処理には、GETリクエストであってもuseMutationを使う方が適しているのではないかと思いました。

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

useQueryを利用する場合
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>
  );
};

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

このケースでは、ボタンクリック時にデータを取得する必要があるため、enabled: falseでレンダー時のフェッチを抑制するよりも、ユーザインタラクションに応じたデータ取得としてuseMutationを使用する方が、処理の意図が明確になります。

useMutationを利用する場合
const useTodo = () => {
  return useMutation({
    mutationFn: fetcher,
  });
};

export const Component = () => {
  const { data, isPending, mutate } = useTodo();
  const handleClick = () => mutate();

  return (
    <div>
      <button onClick={handleClick}>取得!</button>
      ...

ただし、useMutationを使用する場合は、取得したデータがコンポーネント間でキャッシュ共有されない点などに注意が必要です。

useQueryuseMutationはそれぞれの特性を活かした使い分けが重要です。データの再利用性、自動再検証、キャッシュ戦略などを考慮した上で、アプリケーションに適した選択をすることをお勧めします。

もしご意見や改善点がありましたら、ぜひコメントなどでご指摘ください。

Discussion