Skip to content

useQueryでのデバウンス実装方法

この記事が解決する問題

React QueryのuseQueryで検索やフィルタリング操作を実装する際、ユーザーの入力が頻繁に変わる状況では、APIリクエスト回数が増加してパフォーマンスが低下します。この記事ではクエリ実行を最適化するデバウンス実装方法を解説します。

問題の本質

デバウンスを単純にuseQueryのクエリ関数に適用しようとすると次のような問題が発生します:

js
// ❌ 誤った実装例
const debouncedFetch = _.debounce(fetchProducts, 500);
const query = useQuery({ 
  queryKey: ['products'], 
  queryFn: debouncedFetch // エラー発生: 定義された値を返さない
});

このエラーはuseQueryPromiseを直接返す関数を期待しているのに対し、debounceが返すのはタイマー管理用の特殊関数であるため発生します。

効果的な解決策

方法1: クエリキー経由でのデバウンス(推奨)

最も堅牢な方法は、デバウンスされた値をクエリキーに使用することです:

tsx
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クライアント使用時に有効な方法:

tsx
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ライブラリを使ったシンプルな実装:

bash
npm install use-debounce
tsx
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)} />;
};

ベストプラクティス

ユーザビリティ向上テクニック

tsx
const query = useQuery({
  queryKey: ['products', debouncedQuery],
  queryFn: fetchProducts,
  placeholderData: keepPreviousData, // 新しいデータ取得中に前回データを表示
  staleTime: 1000 * 10, // 10秒間はキャッシュを再利用
});

デバウンス時間の設定基準

機能タイプ推奨遅延時間適用例
即時フィードバック100-200msインスタント検索
一般フォーム300-500msフィルタリング操作
API負荷軽減800-1000ms複雑集計クエリ

パフォーマンス向上のポイント

tsx
// ⚠️ 避けるべきパターン
useEffect(() => {
  if (debouncedQuery) {
    query.refetch(); 
    // 不要な再レンダリングと二重取得の危険性
  }
}, [debouncedQuery]);

代替手法の評価

useDeferredValue (React 18以降)

tsx
const [searchTerm, setSearchTerm] = useState('');
const deferredSearchTerm = useDeferredValue(searchTerm);

const query = useQuery({
  queryKey: ['products', deferredSearchTerm],
  queryFn: fetchProducts,
});

useDeferredValueの特徴

  • 優先度制御: 緊急更新に割り込まずバックグラウンドで適用
  • レンダリング最適化: Reactのスケジューリング機構と統合
  • シンプル実装: 追加ライブラリ不要

制限事項

  • データ取得自体はキャンセルされない
  • リクエスト回数削減の効果はデバウンスと比べて劣る

結論

デバウンスをReact Queryで実装する最適解は:

  1. デバウンス値をクエリキーに直結
    useQueryの設計原理に沿った最もシンプルな方法

  2. ユーザーエクスペリエンス向上
    placeholderData: keepPreviousDataでデータ切替を自然に実現

  3. API負荷軽減
    適切なデバウンス時間設定(300-500ms推奨)

tsx
// 完成形サンプル
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が得られます。