useQueryでのデバウンス実装方法
この記事が解決する問題
React QueryのuseQuery
で検索やフィルタリング操作を実装する際、ユーザーの入力が頻繁に変わる状況では、APIリクエスト回数が増加してパフォーマンスが低下します。この記事ではクエリ実行を最適化するデバウンス実装方法を解説します。
問題の本質
デバウンスを単純にuseQuery
のクエリ関数に適用しようとすると次のような問題が発生します:
// ❌ 誤った実装例
const debouncedFetch = _.debounce(fetchProducts, 500);
const query = useQuery({
queryKey: ['products'],
queryFn: debouncedFetch // エラー発生: 定義された値を返さない
});
このエラーはuseQuery
がPromiseを直接返す関数を期待しているのに対し、debounce
が返すのはタイマー管理用の特殊関数であるため発生します。
効果的な解決策
方法1: クエリキー経由でのデバウンス(推奨)
最も堅牢な方法は、デバウンスされた値をクエリキーに使用することです:
import { useState, useEffect } from 'react';
function useDebouncedValue<T>(value: T, delay: number): T {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => clearTimeout(handler);
}, [value, delay]);
return debouncedValue;
}
// 実装例
const SearchComponent = () => {
const [searchTerm, setSearchTerm] = useState('');
const debouncedSearchTerm = useDebouncedValue(searchTerm, 300);
const productsQuery = useQuery({
queryKey: ['products', debouncedSearchTerm], // デバウンス値が変化時のみ再取得
queryFn: () => fetchProducts(debouncedSearchTerm),
placeholderData: keepPreviousData // 新しいデータ取得中は前回の結果を表示
});
return (
<input
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
placeholder="製品を検索..."
/>
);
};
この方法の利点
- キャッシュ有効活用: 同じクエリキーに対する結果を自動キャッシュ
- リクエスト最適化: クエリキーの変化時のみのデータ取得でAPI負荷低減
- コードシンプル: React Queryの仕組みに自然に適合
方法2: AbortSignalによるリクエストキャンセル
axios
などのキャンセル可能なHTTPクライアント使用時に有効な方法:
const fetchProducts = async ({ signal }: { signal?: AbortSignal }) => {
await new Promise(resolve => setTimeout(resolve, 500)); // 500ms待機
if (signal?.aborted) return; // キャンセルされた場合は処理中断
const response = await axios.get('/api/products', {
params: { search },
signal // キャンセルシグナルを渡す
});
return response.data;
};
// 使用例
const query = useQuery({
queryKey: ['products', searchTerm],
queryFn: fetchProducts, // AbortSignal付き関数
staleTime: 1000,
});
注意点
- タイムアウト制御が必要: 待機時間が短すぎると頻繁なリクエストが発生
- 複雑性増加: キャンセルロジックの実装が必要
- 互換性問題: すべてのHTTPクライアントがAbortControllerをサポートするわけではない
サードパーティライブラリ使用例
use-debounce
ライブラリを使ったシンプルな実装:
npm install use-debounce
import { useDebounce } from 'use-debounce';
const SearchComponent = () => {
const [text, setText] = useState('');
const [debouncedText] = useDebounce(text, 300);
const query = useQuery({
queryKey: ['search', debouncedText],
queryFn: () => fetchResults(debouncedText),
});
return <input value={text} onChange={e => setText(e.target.value)} />;
};
ベストプラクティス
ユーザビリティ向上テクニック
const query = useQuery({
queryKey: ['products', debouncedQuery],
queryFn: fetchProducts,
placeholderData: keepPreviousData, // 新しいデータ取得中に前回データを表示
staleTime: 1000 * 10, // 10秒間はキャッシュを再利用
});
デバウンス時間の設定基準
機能タイプ | 推奨遅延時間 | 適用例 |
---|---|---|
即時フィードバック | 100-200ms | インスタント検索 |
一般フォーム | 300-500ms | フィルタリング操作 |
API負荷軽減 | 800-1000ms | 複雑集計クエリ |
パフォーマンス向上のポイント
// ⚠️ 避けるべきパターン
useEffect(() => {
if (debouncedQuery) {
query.refetch();
// 不要な再レンダリングと二重取得の危険性
}
}, [debouncedQuery]);
代替手法の評価
useDeferredValue
(React 18以降)
const [searchTerm, setSearchTerm] = useState('');
const deferredSearchTerm = useDeferredValue(searchTerm);
const query = useQuery({
queryKey: ['products', deferredSearchTerm],
queryFn: fetchProducts,
});
useDeferredValueの特徴
- 優先度制御: 緊急更新に割り込まずバックグラウンドで適用
- レンダリング最適化: Reactのスケジューリング機構と統合
- シンプル実装: 追加ライブラリ不要
制限事項
- データ取得自体はキャンセルされない
- リクエスト回数削減の効果はデバウンスと比べて劣る
結論
デバウンスをReact Queryで実装する最適解は:
デバウンス値をクエリキーに直結
useQuery
の設計原理に沿った最もシンプルな方法ユーザーエクスペリエンス向上
placeholderData: keepPreviousData
でデータ切替を自然に実現API負荷軽減
適切なデバウンス時間設定(300-500ms推奨)
// 完成形サンプル
const [searchTerm, setSearchTerm] = useState('');
const debouncedTerm = useDebouncedValue(searchTerm, 350);
const { data, isLoading } = useQuery({
queryKey: ['search', debouncedTerm],
queryFn: () => fetchAPI(debouncedTerm),
placeholderData: keepPreviousData,
staleTime: 5000,
});
このパターンを適用することでユーザー入力に応じたリアルタイム検索機能をパフォーマンス劣化なしに実現できます。React Queryのキャッシュ機構と組み合わせることで最適なUXが得られます。