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でハマった落とし穴

Posted at

1. queryKeyの設計が雑でキャッシュがバグる

問題queryKey にオブジェクトや関数をそのまま使うとキャッシュが効かなくなる

useQuery({
  queryKey: ['user', { id }],
  queryFn: () => fetchUser(id),
});

原因

queryKey配列の中の値を「参照」で比較してキャッシュを判断する

  • JavaScriptではオブジェクトや配列は「値」ではなく「参照」で比較される。
  • ['user', { id: 1 }]{ id: 1 } は毎回新しく生成されるため、
    → 同じように見えても毎回別物として扱われる。

② そのため、queryKeyが毎回「違うもの」として扱われ、キャッシュが使われない

  • TanStack Query は queryKey を元にキャッシュを探す。
  • ['user', { id: 1 }] !== ['user', { id: 1 }] → 毎回新しいクエリとして実行されてしまう。
  • 本来ならキャッシュを使えるのに、意図せず毎回APIを叩く羽目になる。

解決策:オブジェクトを避けて、プリミティブな値だけで構成する

queryKey: ['user', 1]; 

2. mutation後にuseQueryが自動更新されないと思って焦る

  • 問題useMutation 後に useQuery の内容が古いまま

  • 原因:mutationを実行しても、それに関連するquerykeyは自動で更新されない。

  • 解決策:手動で invalidateQueries を呼ぶ

    const queryClient = useQueryClient();
    
    useMutation({
      mutationFn: updateUser,
      onSuccess: () => {
        queryClient.invalidateQueries({ queryKey: ['user', userId] });
      },
    });
    

これで、['users'] クエリが再取得され、最新の一覧が表示されるようになります。
つまり、「TanStack Queryは賢いけど、そこまでは自動でやってくれない」 という落とし穴の話です。


3. loadingとfetchingの違いを知らずにバグる

  • 問題:「初回の読み込み」だけでローディングUIを表示したいのに、再取得時にも同じローディングUIが表示されてしまい、画面が不自然にリセットされてしまう。

  • 原因useQuery の状態フラグ isFetching は「再取得中」でも true になる。

  • 初回のデータ取得中かどうかを判断するには、isLoading を使う。

具体例:

const { data, isLoading, isFetching } = useQuery({ ... });

if (isLoading) {
  return <Skeleton />;
}

return (
  <div>
    {isFetching && <SmallSpinner />}
    <UserList data={data} />
  </div>
);

4. refetchのタイミングが思ったより多くてパフォーマンスが悪化

  • 問題:意図していない場面でクエリが自動的に再取得されてしまい、APIの呼び出しが増えたり、画面のパフォーマンスが低下してしまう。
  • 原因:予期せぬタイミングで「データが古い」と判断されて、再取得が走っている
  • 解決策①staleTime を設定する
    デフォルトでは staleTime = 0 なので、データは取得した瞬間に stale(古い)扱いになります。
useQuery({
  queryKey: ['users'],
  queryFn: fetchUsers,
  staleTime: 1000 * 60 * 5, // 5分間は再取得しない
});
  • 解決策②refetchOnWindowFocusfalse にする
    React Query はユーザーがタブを切り替えて戻ってきた時に、最新の情報を自動で再取得する。
    refetchOnWindowFocusでデフォルトの再取得をfalseにして、必要なタイミングでtrueにして再取得をする。
useQuery({
  queryKey: ['user'],
  queryFn: fetchUser,
  refetchOnWindowFocus: false,
});
  • 解決策③:クエリキーを安定させる(外から渡された props / state が不安定)
    useQuery の中で queryKey をしっかり制御しているつもりでも、
    外から渡された props や state の参照が毎回変わっていたら、意図せず再取得が発生する。

❌ 具体例:親コンポーネントから渡されたオブジェクト

// 親コンポーネント
<MyComponent filters={{ status: 'done', sort: 'desc' }} />
// 子コンポーネント
const { data } = useQuery({
  queryKey: ['items', filters], // ← filters は props で渡されたオブジェクト
  queryFn: fetchItems,
});

✅ 具体例:useMemo を使って props を安定化させる

const memoizedFilters = useMemo(() => ({ status: 'done', sort: 'desc' }), []);
<MyComponent filters={memoizedFilters} />

「useQuery 内の制御だけでは不十分」
なぜなら queryKey に使っている値が「外から来ている」なら、その安定性は親側に依存するから。

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?