Skip to content

useQuery 防抖实现方案

在 React Query 中使用防抖功能时,直接对查询函数进行防抖会导致「query function must return a defined value」错误,因为 useDebounce 会返回一个 Promise 而不是具体值。本文将介绍三种正确的防抖实现方法。

问题本质

在原生实现中,开发者尝试使用 Lodash 的 _.debounce 包裹查询函数:

jsx
const debouncedFetchProducts = _.debounce(fetchProducts, 500);

useQuery({
  queryKey: ['products', ...deps],
  queryFn: debouncedFetchProducts // 错误用法
});

这会导致错误,因为:

  • useQuery 要求查询函数直接返回有效值(或 Promie)
  • debouncedFetchProducts 返回的是 Promise 包装器而非实际函数
  • 异步防抖机制与 React Query 的执行机制冲突

解决方案一:防抖参数而非函数(推荐)

核心思路

查询参数防抖而非查询函数本身,利用查询键(queryKey)的变化自动触发请求:

jsx
import { useDebounce } from 'use-debounce';

function ProductList() {
  const [search, setSearch] = useState('');
  const [debouncedSearch] = useDebounce(search, 300); // 防抖参数

  const { data } = useQuery({
    queryKey: ['products', debouncedSearch], // 参数变化自动触发查询
    queryFn: () => fetchProducts(debouncedSearch),
    placeholderData: keepPreviousData // 保持上一数据平滑过渡
  });

  return (
    <input
      value={search}
      onChange={(e) => setSearch(e.target.value)} 
    />
  );
}

优势分析

  1. 逻辑清晰:遵循 React Query 设计模式
  2. 自动缓存:参数级缓存策略
  3. 请求取消:内置参数变化自动取消机制
  4. 资源高效:避免额外计时器开销

解决方案二:AbortController + 延时

实现原理

利用 React Query 的请求取消机制实现防抖效果:

jsx
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));

useQuery({
  queryKey: ['products', ...deps],
  queryFn: async ({ signal }) => {
    await delay(400); // 模拟防抖等待
    
    if(signal.aborted) return; // 请求已取消则中止
    
    return axios.get('/api/products', { signal }); 
  }
});

使用场景

适用于需要精细控制请求流的场景:

  • 高频率复杂操作
  • 大文件数据传输
  • 前后请求有依赖关系

注意事项

  1. 需显式传递 signal 给 axios
  2. 合理设置 staleTime 避免重复请求
  3. delay 时间应小于请求超时时间

解决方案三:React 原生优化方案

useDeferredValue

React 18+ 的原生优化 Hook,适合性能敏感场景:

jsx
function SearchComponent() {
  const [search, setSearch] = useState('');
  const deferredSearch = useDeferredValue(search); // 延迟值

  const query = useQuery({
    queryKey: ['search', deferredSearch],
    queryFn: fetchResults
  });

  return (
    <input value={search} onChange={e => setSearch(e.target.value)} />
  );
}

特性对比

方案适用版本实现复杂度适用场景
参数防抖React 16+★☆☆☆☆通用搜索场景
AbortControllerReact Query v4+★★★☆☆高频复杂操作
useDeferredValueReact 18+★★☆☆☆长列表渲染优化

最佳实践建议

  1. 优先参数防抖:90% 场景下的最佳选择
jsx
// 多参数防抖示例
const [params, setParams] = useState();
const debouncedParams = useDebounce(params, 300);

useQuery({
  queryKey: ['data', debouncedParams],
  queryFn: fetchData
});
  1. 开启缓存保留
jsx
useQuery({
  queryKey: ['data', debouncedValue],
  queryFn: fetchData,
  placeholderData: keepPreviousData, // 平滑过渡数据
  staleTime: 1000 // 防抖时间内使用缓存
});
  1. 优化高频场景
jsx
// 结合节流(throttle)避免极端情况
const throttledSetSearch = throttle(setSearch, 100);

SEO 优化提示

对于搜索功能,建议将防抖参数同步至 URL:

jsx
const debouncedSearch = useDebounce(search, 300);

useEffect(() => {
  const url = new URL(window.location);
  url.searchParams.set('q', debouncedSearch);
  window.history.pushState({}, '', url);
}, [debouncedSearch]);

调试技巧

通过 React Query Devtools 观察防抖效果:

jsx
import { ReactQueryDevtools } from '@tanstack/react-query-devtools';

function App() {
  return (
    <>
      {/* 组件内容 */}
      <ReactQueryDevtools initialIsOpen={false} />
    </>
  );
}

结论

错误做法正确做法
_.debounce(queryFn)useDebounce(queryKey)
直接修改查询函数隔离参数处理层
忽视请求取消利用 AbortController

通过将防抖逻辑应用于查询参数而非查询函数,既能满足性能优化需求,又能完美契合 React Query 的工作机制。对于复杂场景可结合 AbortController 或 React 原生优化方案灵活应用。