方案探索 · Birchline Web 客户端

给搜索加防抖的三种思路

Prompt 在我们的 React 代码库里,给任务过滤输入框做一个带防抖的搜索——给我三种不同的实现方式,并列出各自的权衡。

01组件内联 useEffect + setTimeout

防抖逻辑直接写在持有输入框的组件里。

export function TaskSearch() {
  const [draft, setDraft] = useState('');
  const [query, setQuery] = useState('');

  useEffect(() => {
    const id = setTimeout(() => setQuery(draft), 300);
    return () => clearTimeout(id);
  }, [draft]);

  const { data } = useTasks({ search: query });

  return (
    <input
      value={draft}
      onChange={(e) => setDraft(e.target.value)}
      placeholder="筛选任务…"
    />
  );
}
优点
缺点
不需要学新抽象
每个用到搜索的地方都得重写一遍
在 devtools 里好单步调试
同一个概念被拆成两份 state
不引入依赖,bundle 不变
延迟常量藏在组件内部,不易调整
Bundle 影响:+0 kb 可测试性:中等 复用度: SSR 安全:

02自定义 useDebounce Hook

把定时器抽到 src/hooks/ 下的共享 hook 里。

// src/hooks/useDebounce.ts
export function useDebounce<T>(value: T, ms = 300): T {
  const [debounced, setDebounced] = useState(value);
  useEffect(() => {
    const id = setTimeout(() => setDebounced(value), ms);
    return () => clearTimeout(id);
  }, [value, ms]);
  return debounced;
}

// TaskSearch.tsx
const [draft, setDraft] = useState('');
const query = useDebounce(draft, 300);
const { data } = useTasks({ search: query });
优点
缺点
一次 import,过滤、命令栏、看板搜索都能复用
多一个文件需要维护和写文档
配合 fake timer,单测非常容易写
泛型 T 让意图略微模糊
延迟时间作为参数显式暴露,方便调
每次按键依然会触发重新渲染
Bundle 影响:+0.2 kb 可测试性: 复用度: SSR 安全:

03引入小型外部库

引入 use-debounce,值和回调都用它。

import { useDebouncedCallback }
  from 'use-debounce';

export function TaskSearch() {
  const [query, setQuery] = useState('');

  const onChange = useDebouncedCallback(
    (next: string) => setQuery(next),
    300,
    { leading: false, maxWait: 1000 },
  );

  const { data } = useTasks({ search: query });

  return (
    <input
      defaultValue=""
      onChange={(e) => onChange(e.target.value)}
    />
  );
}
优点
缺点
leading / trailing / maxWait 都帮你处理好了
新增运行时依赖,需要定期审计和升级
回调形式跳过了中间的重渲染
非受控输入框,和 Birchline 的表单风格不一致
边界情况(unmount、flush、cancel)测试充分
为一个十行能搞定的东西付 ~1.4 kb gzip
Bundle 影响:+1.4 kb 可测试性: 复用度: SSR 安全: