React 19 入门教程
Hook 规则与进阶
本教程共 50 篇 · 第 34 篇 · 更新于 2026-07-29 · 约 7 分钟阅读
ReactHook规则ESLintuseIduseLayoutEffect
34. Hook 规则与进阶
本节目标:掌握 Hook 的两条核心规则,了解原理和进阶用法,写出符合规范的 Hook 代码。
规则一:只在顶层调用
不在循环、条件、嵌套函数里调用 Hook:
// 错误:条件里调用 Hook
function Form() {
if (isLoggedIn) {
const [user, setUser] = useState(null); // 不行!
}
}
// 错误:循环里调用 Hook
function List({ items }) {
items.forEach(item => {
const [expanded, setExpanded] = useState(false); // 不行!
});
}
// 正确:Hook 在组件顶层
function Form() {
const [user, setUser] = useState(null);
if (isLoggedIn) { /* ... */ }
}
为什么不能在条件里调用
React 靠 Hook 的调用顺序来关联状态。条件导致顺序变化,状态就乱套了。
规则二:只在函数组件或自定义 Hook 里调用
// 错误:普通函数里调用
function fetchData() {
const [data, setData] = useState(null); // 不行!
}
// 错误:类组件里调用
class MyComponent extends React.Component {
render() {
const [count, setCount] = useState(0); // 不行!
}
}
// 正确:函数组件
function MyComponent() {
const [count, setCount] = useState(0);
}
// 正确:自定义 Hook
function useMyHook() {
const [count, setCount] = useState(0);
}
用 ESLint 插件自动检查
安装 eslint-plugin-react-hooks,它会自动检测违规:
npm install eslint-plugin-react-hooks --save-dev
// eslint.config.js
import reactHooks from 'eslint-plugin-react-hooks';
export default [
plugins: {
'react-hooks': reactHooks,
},
rules: {
'react-hooks/rules-of-hooks': 'error', // 检查规则
'react-hooks/exhaustive-deps': 'warn', // 检查 effect 依赖
},
];
别忽略 exhaustive-deps 警告
这个规则会检查 useEffect 的依赖是否完整。偶尔可以抑制,但要确认自己在做什么。
Hook 的原理(简化版)
React 内部用一个链表存储 Hook 状态。每次渲染,React 按顺序遍历链表:
第一次渲染:
useState(0) → 节点1: { state: 0 }
useEffect(...) → 节点2: { effect: fn }
第二次渲染:
useState(0) → 节点1: { state: 0 } // 按顺序匹配
useEffect(...) → 节点2: { effect: fn }
如果第二次渲染时,第一个 useState 前面插了个 useState,链表就对不上了。这就是为什么不能在条件里调用 Hook。
useId:生成唯一 ID
useId 生成稳定唯一的 ID,适合表单 label 关联:
import { useId } from 'react';
function Form() {
const id = useId();
return (
<>
<label htmlFor={id}>用户名</label>
<input id={id} />
</>
);
}
多个组件使用也不会冲突:
function App() {
return (
<>
<Form /> // id: ":r0:"
<Form /> // id: ":r1:"
</>
);
}
useId 的 ID 格式
生成的 ID 形如
:r0:、:r1:,带冒号。不能用在 CSS 选择器里。
useLayoutEffect:浏览器绘制前执行
和 useEffect 类似,但在浏览器绘制前同步执行。适合需要读取 DOM 布局的场景:
import { useLayoutEffect, useRef, useState } from 'react';
function Tooltip() {
const ref = useRef(null);
const [tooltipHeight, setTooltipHeight] = useState(0);
useLayoutEffect(() => {
const { height } = ref.current.getBoundingClientRect();
setTooltipHeight(height); // 在绘制前更新,避免闪烁
}, []);
return <div ref={ref}>...</div>;
}
useLayoutEffect 会阻塞渲染
大多数情况用 useEffect。只有”读取布局 → 立即更新 → 避免闪烁”时才用 useLayoutEffect。
useImperativeHandle:暴露组件方法
配合 forwardRef,让父组件调用子组件的方法:
import { forwardRef, useImperativeHandle, useRef } from 'react';
const FancyInput = forwardRef(function FancyInput(props, ref) {
const inputRef = useRef(null);
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => { inputRef.current.value = ''; }
}), []);
return <input ref={inputRef} {...props} />;
});
// 父组件
function Parent() {
const ref = useRef(null);
return (
<>
<FancyInput ref={ref} />
<button onClick={() => ref.current.focus()}>聚焦</button>
<button onClick={() => ref.current.clear()}>清空</button>
</>
);
}
useDebugValue:自定义调试标签
在 React DevTools 里显示自定义标签:
function useOnlineStatus() {
const [isOnline, setIsOnline] = useState(true);
// ...
useDebugValue(isOnline ? '在线' : '离线');
return isOnline;
}
DevTools 里会显示 useOnlineStatus: 在线,方便调试。
Hook 最佳实践
- 自定义 Hook 抽重复逻辑:两个组件有相同逻辑,提取成 Hook
- Hook 保持简单:一个 Hook 只做一件事
- 命名清晰:
useUserPosts比useData好 - 返回值一致:数组或对象,选一种风格坚持
- 文档注释:复杂 Hook 加 JSDoc 说明
/**
* 订阅在线状态
* @returns {boolean} 当前是否在线
*/
function useOnlineStatus() {
// ...
}
常见错误排查
| 错误 | 原因 | 解决 |
|---|---|---|
| ”Rendered more hooks than previous render” | 条件/循环里调 Hook | 移到顶层 |
| ”Invalid hook call” | 在普通函数/类组件里调 Hook | 只在函数组件调 |
| effect 无限循环 | 依赖数组缺项或对象依赖 | 检查依赖 |
| 状态不同步 | 闭包捕获旧值 | 用函数式更新 |
本节要点
- 只在顶层调用 Hook,不在条件/循环里调用
- 只在函数组件或自定义 Hook 里调用
- ESLint 插件自动检查
- useId 生成唯一 ID,useLayoutEffect 在绘制前执行
- 自定义 Hook 是逻辑复用的最佳方式