R29第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

自定义 Hook 的边界

自定义 Hook 的边界 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

两处组件写着一样的订阅代码,还总有一处忘清理——该抽自定义 Hook 了。这一课你会把订阅和表单逻辑抽成职责单一的 Hook,定义好输入输出,并学会判断「什么时候不该抽」。

问题场景

「页头」和「设置页」都要显示网络在线状态,各自写了一遍:

// 页头里
const [isOnline, setIsOnline] = useState(true);
useEffect(() => {
  const update = () => setIsOnline(navigator.onLine);
  window.addEventListener('online', update);
  window.addEventListener('offline', update);
  return () => {
    window.removeEventListener('online', update);
    window.removeEventListener('offline', update);
  };
}, []);

// 设置页里:同一段又抄了一遍,而且忘了 removeEventListener

抄第三遍之前,把这段逻辑收编成一个 Hook。

核心解释

自定义 Hook 就是一个名字以 use 开头、内部调用了其他 Hook 的普通函数。它不魔法:组件 A 和组件 B 各自调用 useOnlineStatus(),各自持有独立的 state,互不相通——复用的是逻辑,不是状态。

设计一个 Hook 和设计一个函数一样,关键是把三件事说清楚:

  • 输入:参数。比如 useField(initial) 的初始值、useInterval(callback, delay) 的间隔。
  • 输出:返回值。通常是「数据 + 操作」:{ isOnline },或 { value, onChange, reset }。
  • 清理职责:Hook 内部发起的订阅、计时器,由 Hook 自己在 Effect 清理函数里收尾,调用方不应操心。

边界同样重要:一个 Hook 只管一件事。订阅在线状态、管理表单字段、轮询接口是三件事,就该是三个 Hook;页面把它们组合起来,组合发生在组件里,而不是把三坨逻辑全塞进一个 usePageData() 大杂烩——那只是把上帝组件换成了上帝 Hook。

示例拆解

function useOnlineStatus() {
  const [isOnline, setIsOnline] = useState(true);

  useEffect(() => {
    const update = (e) => setIsOnline(e.type === 'online');
    window.addEventListener('online', update);
    window.addEventListener('offline', update);
    return () => {
      window.removeEventListener('online', update);
      window.removeEventListener('offline', update);
    };
  }, []);

  return isOnline; // 输出:一个布尔值
}

function useField(initial) {
  const [value, setValue] = useState(initial);
  return {
    value,
    onChange: (e) => setValue(e.target.value),
    reset: () => setValue(initial),
  }; // 输出:数据 + 操作
}

function CourseForm() {
  const isOnline = useOnlineStatus(); // 订阅:Hook 管
  const name = useField('');          // 表单:Hook 管
  if (!isOnline) return <p>离线中,暂时不能提交</p>;

  return (
    <form onSubmit={(e) => { e.preventDefault(); name.reset(); }}>
      <input value={name.value} onChange={name.onChange} placeholder="课程名" />
      <button type="submit">保存</button>
    </form>
  );
}
  • useOnlineStatus 输入为空、输出一个布尔值,清理封装在内部,任何组件一行代码接入。
  • useField 把「受控输入三件套」打包,几个字段就调用几次,状态各自独立。
  • CourseForm 只剩组合与渲染,看不到任何订阅细节。

动手任务

打开本课实验区:

  1. 点「模拟断网 / 恢复」,验证页头、表单、底部状态条三处同步切换——它们各自调用了同一个 useOnlineStatus。
  2. 给 useField 加一条校验:返回值里多一个 error(为空时提示「必填」)。
  3. 挑一个你写过的组件,找出其中「订阅 / 表单 / 轮询」混杂的逻辑,口述拆成几个 Hook、各自输入输出是什么。

常见错误

  • 在条件或循环里调用自定义 Hook:它内部是 Hooks,一样受「只在顶层调用」的规则约束(R33 展开)。
  • 以为两次调用共享状态:每个组件、每次调用都是独立的状态实例;要共享得用 Context 或外部 store(R31)。
  • 抽成「上帝 Hook」:usePageData 返回十几个值、内部管五件事——按领域拆开,一个 Hook 一件事。
  • Hook 内部的订阅不清理:逻辑收进 Hook 不等于责任消失,removeEventListener、clearInterval 一个都不能少。

检查与复盘

完成下方检查题确认理解。自查清单:

  • 能说出自定义 Hook 的本质:调用其他 Hook 的函数,复用逻辑不复用状态
  • 每个 Hook 能用一句话说清输入、输出、清理职责
  • 新需求出现时先问:这是新逻辑,还是已有 Hook 的组合?

参考资料

动手实验
动手实验
预览
点击「运行」在隔离沙箱中预览
检查题共 1 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1单选中等

两个组件都调用了自定义 Hook useWindowWidth()(内部用 useState + useEffect 监听 resize)。它们之间共享了什么?

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...