自定义 Hook

聊聊这个让代码复用到飞起的神器

先说结论:自定义 Hook 就是个普普通通的 JavaScript 函数,只不过名字得用 use 开头,里面可以调用其他 Hook 而已。

但别小看这个"只不过",它可是 React 里逻辑复用的终极武器。

自定义 Hook 能干啥?

想象一下:你在好几个组件里写了类似的代码——都是监听窗口大小、都是处理表单输入、都是请求接口数据……

这时候你就可以把这部分逻辑抽出来,做成一个自定义 Hook。

// 抽出来的自定义 Hook
function useWindowSize() {
  const [size, setSize] = useState({ width: 0, height: 0 });

  useEffect(() => {
    const handleResize = () => {
      setSize({ width: window.innerWidth, height: window.innerHeight });
    };
    handleResize();
    window.addEventListener("resize", handleResize);
    return () => window.removeEventListener("resize", handleResize);
  }, []);

  return size;
}

然后在组件里直接用:

function MyComponent() {
  const { width, height } = useWindowSize();
  return (
    <div>
      窗口大小: {width} x {height}
    </div>
  );
}

好处就是:

  • 组件只需要专注渲染 UI,逻辑都丢给 Hook
  • 代码更清晰,改逻辑不用满世界找
  • 多个组件想用直接调用,复制粘贴再见

必须遵守的规则

1. 名字必须以use 开头

这不是建议,是强制的。

React 的 ESLint 插件就靠这个 use 前缀来检测你是不是在正确的地方调用 Hook。没有这个前缀,React 才不管你内部调用了什么,直接把你当普通函数处理,规则检查全失效,bug 就找上门了。

// ❌ 错误:没有 use 前缀
function getWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth });
  return size;
}

// ✅ 正确:带 use 前缀
function useWindowSize() {
  const [size, setSize] = useState({ width: window.innerWidth });
  return size;
}

2. 只搞逻辑,别搞 UI

这是自定义 Hook 和组件的根本区别:

  • 组件:负责渲染 UI
  • 自定义 Hook:负责带状态的逻辑

所以你的 Hook 应该返回的是值和方法,而不是 JSX:

// ❌ 错误:返回了 JSX,这应该是组件该干的事
function useButton() {
  return <button>点我</button>;
}

// ✅ 正确:只返回状态和操作
function useToggle(initialValue = false) {
  const [value, setValue] = useState(initialValue);
  const toggle = () => setValue((v) => !v);
  return [value, toggle];
}

3. 每个组件调用,都是独立的状态

这点很重要:你在两个不同组件里调用同一个自定义 Hook,它们的状态是独立的,互不影响

function A() {
  const [count, setCount] = useCounter(); // 这是 A 的 count
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}

function B() {
  const [count, setCount] = useCounter(); // 这是 B 的 count,跟 A 没关系
  return <button onClick={() => setCount((c) => c + 1)}>{count}</button>;
}

自定义 Hook 只是复用了一段代码逻辑,而不是共享状态本身。除非你在 Hook 里接了 Redux、Zustand 这些全局状态库。

怎么写好一个自定义 Hook?

1. 单一职责,一个 Hook 只干一件事

// ❌ 一个 Hook 干了两件事
function useUserData() {
  const [user, setUser] = useState(null);
  const [theme, setTheme] = useState("light");
  // ...
}

// ✅ 拆成两个,各干各的
function useUserData() {
  /* 用户数据相关 */
}
function useTheme() {
  /* 主题相关 */
}

一个 Hook 做得越小越好,用的时候想组合就组合,灵活得很。

2. 输入输出要清晰

  • 参数:像普通函数一样,通过参数传入配置
  • 返回值:通常返回数组 [value, setValue] 或者对象 { data, loading, error }
// 返回数组 - 简单场景
function useToggle(initial = false) {
  const [value, setValue] = useState(initial);
  return [value, () => setValue((v) => !v)];
}

// 返回对象 - 返回内容多的时候
function useFetch(url) {
  const [data, setData] = useState(null);
  const [loading, setLoading] = useState(true);
  // ...
  return { data, loading, error };
}

3. 方便测试

因为 Hook 不涉及 UI,纯逻辑,所以特别容易写单元测试

// 测试可以这样写
const { result } = renderHook(() => useCounter(10));
act(() => {
  result.current.increment();
});
expect(result.current.count).toBe(11);

不用渲染 DOM,直接测逻辑,干净利落。

总结一下

  • 自定义 Hook = 普通函数 + 必须用 use 开头 + 里面能调 Hook
  • 作用:把带状态的逻辑抽出来复用
  • 规则:名字带 use、只返回逻辑不返回 UI、每次调用状态独立
  • 技巧:一个 Hook 只做一件事、输入输出要清晰、记得写测试