人人都会AI编程

11.2 通用业务 Hooks 封装示例:防抖节流、分页、表单、权限

更新时间:2026-07-10

自定义 Hooks 的真正威力在于将反复出现的业务逻辑抽离成可复用的函数。下面这些 Hooks 几乎会出现在每一个中大型 React 项目中,它们分别解决了数据请求、浏览器存储、防抖节流、状态切换等高频需求。

1. 数据请求:useRequest

手动在每个组件里写 useEffect + fetch 会导致大量重复的 loading、error 处理逻辑。封装一个通用的请求 Hook 可以统一管理异步请求的生命周期。

import { useState, useEffect, useCallback } from 'react';

function useRequest(requestFn, { immediate = true } = {}) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState(null);

  const run = useCallback(async (...args) => {
    setLoading(true);
    setError(null);
    try {
      const result = await requestFn(...args);
      setData(result);
      return result;
    } catch (err) {
      setError(err);
      throw err;
    } finally {
      setLoading(false);
    }
  }, [requestFn]);

  useEffect(() => {
    if (immediate) {
      run();
    }
  }, [immediate, run]);

  return { data, loading, error, run };
}

// 使用
function UserList() {
  const { data: users, loading, error } = useRequest(() =>
    fetch('/api/users').then(res => res.json())
  );

  if (loading) return <Spinner />;
  if (error) return <ErrorMessage error={error} />;
  return <List items={users} />;
}

要点:

  • 通过 run 方法支持手动触发(刷新、翻页等场景)。
  • requestFn 通过参数传入,Hook 本身不耦合具体 API。
  • 实际项目中建议搭配 TanStack Query 处理更复杂的缓存与更新逻辑,但轻量场景下自封装 useRequest 足够灵活。

2. 浏览器存储:useLocalStorage

localStorage 的读写同步到 React 状态,同时保证跨标签页的同步(可选)。

function useLocalStorage(key, initialValue) {
  const [storedValue, setStoredValue] = useState(() => {
    try {
      const item = window.localStorage.getItem(key);
      return item ? JSON.parse(item) : initialValue;
    } catch {
      return initialValue;
    }
  });

  const setValue = (value) => {
    const valueToStore = value instanceof Function ? value(storedValue) : value;
    setStoredValue(valueToStore);
    window.localStorage.setItem(key, JSON.stringify(valueToStore));
  };

  // 监听其他标签页的 storage 变化
  useEffect(() => {
    const handleStorageChange = (e) => {
      if (e.key === key && e.newValue !== null) {
        setStoredValue(JSON.parse(e.newValue));
      }
    };
    window.addEventListener('storage', handleStorageChange);
    return () => window.removeEventListener('storage', handleStorageChange);
  }, [key]);

  return [storedValue, setValue];
}

// 使用
const [theme, setTheme] = useLocalStorage('theme', 'light');

要点:

  • 初始化时从 localStorage 读取,避免每次渲染时 JSON 解析的性能开销。
  • 支持函数式更新,保持与 useState 一致的 API 风格。
  • storage 事件监听实现了多标签页数据同步,适用于主题、登录状态等场景。

3. 防抖与节流:useDebounce / useThrottle

搜索框输入、窗口 resize 等高频触发场景,需要限制回调执行频率。

// 防抖:延迟执行,如果在延迟期间再次触发则重新计时
function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value);

  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay);
    return () => clearTimeout(timer);
  }, [value, delay]);

  return debouncedValue;
}

// 节流函数 Hook
function useThrottle(fn, delay = 200) {
  const lastRun = useRef(Date.now());

  return useCallback((...args) => {
    const now = Date.now();
    if (now - lastRun.current >= delay) {
      fn(...args);
      lastRun.current = now;
    }
  }, [fn, delay]);
}

// 使用
function SearchBox() {
  const [keyword, setKeyword] = useState('');
  const debouncedKeyword = useDebounce(keyword, 500);

  useEffect(() => {
    if (debouncedKeyword) {
      fetchSearchResults(debouncedKeyword);
    }
  }, [debouncedKeyword]);

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

要点:

  • useDebounce 对值进行防抖,常用于搜索联想、表单校验等需要延迟触发的场景。
  • useThrottle 对函数进行节流,常用于滚动事件、拖拽等需要固定频率执行的场景。
  • 两者都通过清理 useEffectuseRef 记录时间戳来避免内存泄漏和时间错乱。

4. 布尔切换:useToggle

看似简单,但写 setState(!state) 时往往会出现闭包陷阱。封装一个安全的切换 Hook 能避免很多低级错误。

function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);

  const toggle = useCallback(() => setValue(v => !v), []);
  const setTrue = useCallback(() => setValue(true), []);
  const setFalse = useCallback(() => setValue(false), []);

  return [value, { toggle, setTrue, setFalse }];
}

// 使用
const [isOpen, { toggle, setFalse }] = useToggle(false);
return (
  <>
    <button onClick={toggle}>切换</button>
    {isOpen && <Modal onClose={setFalse} />}
  </>
);

要点:

  • toggle 使用函数式更新 setValue(v => !v),避免依赖外部状态值,杜绝闭包陷阱。
  • 返回的 setTrue / setFalse / toggle 引用稳定,可以作为 useEffect 的依赖项安全传递。

5. 元素可见性:useIntersectionObserver

懒加载图片、下拉加载更多、曝光埋点等都需要检测某个 DOM 元素是否进入视口。借助 IntersectionObserver API,可以轻松封装。

function useIntersectionObserver(targetRef, options = {}) {
  const [isIntersecting, setIsIntersecting] = useState(false);

  useEffect(() => {
    const target = targetRef.current;
    if (!target) return;

    const observer = new IntersectionObserver(([entry]) => {
      setIsIntersecting(entry.isIntersecting);
    }, options);

    observer.observe(target);
    return () => observer.disconnect();
  }, [targetRef, options.threshold, options.rootMargin]);

  return isIntersecting;
}

// 使用:懒加载图片
function LazyImage({ src, alt }) {
  const imgRef = useRef(null);
  const isVisible = useIntersectionObserver(imgRef, { threshold: 0.1 });

  return (
    <img
      ref={imgRef}
      src={isVisible ? src : ''}
      alt={alt}
      style={{ minHeight: 200, background: '#eee' }}
    />
  );
}

要点:

  • Hook 只关注“是否可见”这个单一状态,与具体业务逻辑解耦。
  • options 支持配置触发阈值和根边距,适配不同提前量需求。
  • 观察器在组件卸载时自动断开,避免内存泄漏。

6. 异步操作状态封装:useAsync

很多场景下,我们只关心一个异步操作的 loading / error / data,并且需要手动触发。useAsyncuseRequest 更轻量,适合表单提交、导出下载等非自动触发的场景。

function useAsync(asyncFn) {
  const [state, setState] = useState({
    status: 'idle', // idle | pending | success | error
    data: null,
    error: null,
  });

  const run = useCallback(async (...args) => {
    setState({ status: 'pending', data: null, error: null });
    try {
      const data = await asyncFn(...args);
      setState({ status: 'success', data, error: null });
      return data;
    } catch (error) {
      setState({ status: 'error', error, data: null });
      throw error;
    }
  }, [asyncFn]);

  return { ...state, run };
}

// 使用:提交表单
const { status, error, run } = useAsync(submitForm);

return (
  <form onSubmit={e => { e.preventDefault(); run(formData); }}>
    <button disabled={status === 'pending'}>
      {status === 'pending' ? '提交中...' : '提交'}
    </button>
    {status === 'error' && <p className="error">{error.message}</p>}
  </form>
);

要点:

  • 状态机设计避免了多个布尔值的混乱,status 明确表达当前阶段。
  • run 返回 Promise,因此可以链式调用或者配合 await,灵活处理后续操作。

封装原则总结

这几个示例覆盖了开发中 80% 的自定义 Hooks 场景。它们都遵循了以下原则:

  1. 单一职责:每个 Hook 只做一件事,命名清晰反映其功能。
  2. 状态与逻辑内聚:将相关的 state、effect 和业务逻辑封装在一起,对外暴露干净的 API。
  3. 与组件解耦:Hook 不依赖任何具体的组件上下文,可以在任何组件中使用。
  4. 通用性与可配置性:通过参数允许不同场景的配置,但不暴露不必要的实现细节。

掌握这些通用 Hooks 的封装模式后,面对新的业务需求时,你自然能够快速抽离出可复用的逻辑,让代码库始终保持干净、可维护。