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

Hook 规则与逻辑组织

Hook 规则与逻辑组织 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

「未登录就不挂这些 Hook」听起来合理,一运行就崩溃。这一课你会理解 Hook 依赖调用顺序的记账原理,修掉条件调用和循环调用,并学会把复杂组件拆成「自定义 Hook + 子组件 + 命名事件处理」的可读结构。

问题场景

课程页第一版:未登录时不渲染标签页,代码顺手把 useState 也包进了条件:

function CoursePage({ user }) {
  if (!user) return <p>请先登录</p>; // 提前返回

  const [tab, setTab] = useState('overview'); // 崩溃:条件调用 Hook
  const [notes, setNotes] = useState([]);

  // ...
}

登录瞬间报错:Rendered fewer hooks than expected。未登录那次渲染执行了 0 个 Hook,登录后 React 期望从第 1 个位置找到 tab 的状态——账对不上了。

核心解释

React 给每个组件维护一张按调用顺序记账的表:第一次渲染里第 1 个调用的 Hook 存 tab、第 2 个存 notes……之后每次渲染按同样的顺序取值。只要顺序稳定,位置就是身份。所以规则只有两条:

  1. 只在顶层调用:不能包在 if、循环、嵌套函数里;提前 return 必须放在所有 Hook 之后。
  2. 只在函数组件或自定义 Hook 里调用:普通 JS 函数里没有这张账表。

条件需求不是不能有,而是要挪进 Hook 内部:useState(user ? user.id : null)、useEffect(() => { if (!user) return; ... }, [user])——Hook 照常调用,逻辑自己分支。

可读性组织有三板斧,按序使用:状态逻辑按领域抽成自定义 Hook(useCourseTabs、useNotes);事件处理写成命名函数(handleTabChange,而不是内联三十行);渲染分支提炼成子组件。组件函数最终读起来像目录,而不是流水账。

示例拆解

修复版加组织后的结构:

function useCourseTabs(user, initial) {
  const [tab, setTab] = useState(initial);
  const [notes, setNotes] = useState([]);

  useEffect(() => {
    if (!user) return; // 条件写在 Hook 内部,Hook 本身照常调用
    setNotes([`${user} 的课程笔记`, 'Hook 规则:只在顶层调用']);
  }, [user]);

  return { tab, setTab, notes }; // 输入 user/initial,输出数据与操作
}

function CoursePage({ user }) {
  const { tab, setTab, notes } = useCourseTabs(user, 'overview'); // 顺序永远稳定

  if (!user) return <p>请先登录</p>; // 现在提前返回是安全的

  function handleTabChange(next) {
    setTab(next);
  }

  return <CourseTabs tab={tab} onChange={handleTabChange} notes={notes} />;
}
  • Hook 全部在顶层、顺序固定,user 变化只是让各 Hook 内部走不同分支。
  • 提前 return 的位置在所有 Hook 调用之后——新手最容易忽视的细节。
  • useCourseTabs 的输入输出保持函数式接口:传 user 进去,返回数据与操作,而不是偷偷读外部变量。

动手任务

打开本课实验区:

  1. 反复点「登录 / 退出登录」,组件在两个分支间往返,确认不崩溃、状态按预期保留或清空。
  2. 打开实验文件里注释掉的「错误示范」,先口头预测崩溃信息,再复制到本地项目实际运行对照。
  3. 把实验里 CoursePage 的笔记逻辑抽成 useNotes(user),组件里只留组合与渲染。

常见错误

  • 提前 return 放在 Hook 之前:最常见的事故源;记住「先 Hook,后 return」。
  • 在自定义 Hook 里条件调用底层 Hook:包装不豁免规则,问题原样传递。
  • 以为 .map() 里渲染组件违规:循环里渲染的是组件(各组件各有账表),不是调用 Hook;但 items.map(() => useState()) 这种在当前组件里循环调用 Hook 就是崩溃代码。
  • 用「这次渲染碰巧没走到」安慰自己:只要存在走得到的另一条路径,顺序就可能错位,崩溃只是时机问题。

检查与复盘

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

  • 能用「按顺序记账」解释条件调用 Hook 为什么会崩
  • 审查组件的固定动作:先扫顶层 Hook 是否无条件执行,再看提前 return 的位置
  • 复杂组件已按「自定义 Hook / 命名事件处理 / 子组件」组织,单函数不再超过一屏

参考资料

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

关于 Hooks 的使用规则(React 19 口径),下列说法正确的有?(多选)

读完这一课了?

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

Command Palette

Search for a command to run...