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分間は再取得しない
});
-
解決策②:
refetchOnWindowFocus
をfalse
にする
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 に使っている値が「外から来ている」なら、その安定性は親側に依存するから。