0
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?

【入門】TanStack Queryで何が減るのか — useEffectと比べてリクエスト回数を数えた

0
Last updated at Posted at 2026-08-31

この記事の対象読者

React でデータ取得を useEffect と useState で書いている人に向けた入門記事です。

  • ローディングとエラーの state を毎回書いていて、面倒だと感じている
  • 同じデータを複数のコンポーネントで使うとき、どう共有するか決めきれていない
  • TanStack Query の名前は知っているが、何が解決されるのか分かっていない

React の基本的なフックが分かれば読めます。

実行結果は React 19.2.8 / TanStack Query 5.102.8 / Vite 8.2.2 / Node.js 22 で確認したものです。回数を数えるため StrictMode は外しています(開発時に effect が2回実行されるため)。

いまの書き方

ユーザー名を表示するだけのコンポーネントを考えます。useEffect で取ってくる、よくある形です。

function UserName() {
  const [user, setUser] = useState<User | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    let cancelled = false;
    setLoading(true);
    fetchUser(1)
      .then((u) => { if (!cancelled) setUser(u); })
      .catch((e) => { if (!cancelled) setError(e); })
      .finally(() => { if (!cancelled) setLoading(false); });
    return () => { cancelled = true; };
  }, []);

  if (loading) return <span>読み込み中</span>;
  if (error) return <span>エラー</span>;
  return <span>{user?.name}</span>;
}

state が3つと、後片付けの cancelled が要ります。取得するデータが増えるたびに、この形を書き写すことになります。

面倒ではありますが、動きます。問題は別のところにあります。

同じデータを2箇所で使うと2回叩く

このコンポーネントを、ヘッダーとサイドバーの2箇所に置きます。

<p>ヘッダー: <UserName /></p>
<p>サイドバー: <UserName /></p>

同じユーザーの情報なので、通信は1回で済むはずです。実際に fetchUser が呼ばれた回数を数えてみます。

リクエスト回数: 2

2回でした。当然で、コンポーネントがそれぞれ独立して useEffect を持っているからです。お互いのことを知りません。

避けるには、取得する場所を親に上げて props で配るか、Context に載せるか、状態管理のライブラリを入れるか、といった判断が要ります。データ取得の都合で、コンポーネントの構造を決めることになります。

useQuery に置き換える

同じものを TanStack Query で書きます。

npm install @tanstack/react-query

アプリ全体を Provider で囲みます。

const queryClient = new QueryClient();

<QueryClientProvider client={queryClient}>
  <App />
</QueryClientProvider>

コンポーネントはこうなります。

function UserName() {
  const { data, isPending, isError } = useQuery({
    queryKey: ['user', 1],
    queryFn: () => fetchUser(1),
  });

  if (isPending) return <span>読み込み中</span>;
  if (isError) return <span>エラー</span>;
  return <span>{data.name}</span>;
}

state の宣言が消えました。cancelled の後片付けも要りません。

置き場所は変えずに、さっきと同じく2箇所に置いて数えます。

リクエスト回数: 1

1回になりました。 コンポーネントの構造は何も変えていません。

なぜ1回になるのか

鍵は queryKey です。

queryKey: ['user', 1]

TanStack Query は、この配列を問い合わせの識別子として扱います。同じキーの問い合わせは同じものとみなされます。

2つのコンポーネントが同時に ['user', 1] を要求すると、Query は「それは今まさに取得中のものと同じだ」と判断して、通信をまとめます。結果は両方に配られます。

データの共有先を親に上げる必要がなかったのは、共有の単位がコンポーネントの木ではなくキーになっているからです。どこに置いても、キーが同じなら同じデータを見ます。

キーに値を含めるのは、区別のためです。['user', 1] と ['user', 2] は別のデータなので、別々に取得されて別々に保持されます。

キャッシュと staleTime

取得したデータは保持されます。画面を離れて戻ってきたときに、何が起きるかを見ます。

一度表示してからアンマウントし、また表示する、という操作をして数えます。

リクエスト回数: 2

1つ増えました。キャッシュがあるのに通信しています。

これは既定の設定によるものです。TanStack Query の staleTime は既定が 0 で、取得した直後から「古いかもしれない」扱いになります。 再表示のきっかけがあると、裏で取り直しにいきます。

このとき画面には、キャッシュに残っていたデータがすぐ表示されます。読み込み中の表示を挟まずに中身が出て、その裏で新しいデータを取ってきて、届いたら差し替える動きです。

取り直しが要らないなら、staleTime を設定します。

useQuery({
  queryKey: ['user', 1],
  queryFn: () => fetchUser(1),
  staleTime: 60_000, // 60秒は新鮮とみなす
});

同じ操作をもう一度します。

リクエスト回数: 1

増えませんでした。 60秒のあいだはキャッシュをそのまま使うので、通信が発生しません。

まとめるとこうなります。

操作 staleTime 既定(0) staleTime 60秒
初回・2コンポーネント 1 1
アンマウントして再表示 +1 +0

この設定がいちばん最初に迷うところだと思います。 更新が頻繁なデータなら既定のままでよく、めったに変わらないマスタデータなら長めに取る、という決め方になります。

キーが変わると取り直す

表示するユーザーを切り替える場合を見ます。キーに id を含めます。

function UserName({ id }: { id: number }) {
  const { data, isPending } = useQuery({
    queryKey: ['user', id],
    queryFn: () => fetchUser(id),
  });

  if (isPending) return <span>読み込み中</span>;
  return <span>{data.name}</span>;
}

id を 1 → 2 → 1 と切り替えて数えます。

操作 累計
id=1 を表示 1
id=2 に切り替え 2
id=1 に戻る 3

id を変えると、キーが変わるので新しい取得が走ります。依存する値をキーに入れておけば、変わったときに取り直されます。 useEffect の依存配列に相当する役割です。

id=1 に戻ったときも1つ増えています。これは前の節の staleTime が既定の 0 だからで、キャッシュは残っているものの古い扱いになり、裏で取り直しにいきます。表示自体はキャッシュから即座に出ます。

状態の扱いが揃う

副次的ですが、効いてくる点があります。

const { data, isPending, isError } = useQuery({ ... });

読み込み中とエラーの表し方が、どのデータ取得でも同じ形になります。useEffect で書いていたときは、命名も判定の順序も書く人と場所によってばらついていました。

新しく取得を追加するときに、判断することが減ります。

まとめ

  • 同じ queryKey の問い合わせはまとめられます。 2コンポーネントから同時に要求しても通信は1回でした
  • 共有の単位がコンポーネントの木ではなくキーなので、データ取得の都合で構造を決めなくて済みます
  • staleTime の既定は 0 です。取得直後から古い扱いになり、再表示のきっかけで取り直します
  • 依存する値をキーに入れておくと、変わったときに取り直されます。useEffect の依存配列にあたる役割です
  • staleTime を設定すると、その間はキャッシュを使って通信しません。再表示しても回数は増えませんでした
  • isPending と isError で、状態の扱いがどの取得でも同じ形になります

自前で書いていたローディングとエラーの state が消えるのは分かりやすい利点ですが、実際に効くのは同じデータを複数箇所で使えるようになることのほうだと思います。取得の重複を避けるために構造を変える、という判断が要らなくなります。

書き込み側の useMutation はこの記事では扱っていません。まずは読み取りだけ置き換えてみるのが入りやすいと思います。

0
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
0
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?