Hooks
Hooks 是 React 16.8 引入的函数组件能力扩展。它们让你在不写 class 的情况下使用 state 和其他 React 特性。
Hooks 规则
两条铁律
- 只在顶层调用 — 不要在循环、条件或嵌套函数中调用 Hook
- 只在 React 函数中调用 — 函数组件或自定义 Hook 中,不要在普通 JS 函数中调用
🔬 深入原理:React 通过调用顺序将 Hook 与对应的 Fiber 节点关联。Hooks 在 Fiber 节点中是一个单向链表(
memoizedState)。如果你在条件中调用 Hook,链表顺序会乱,导致状态错位。
为什么要有这些规则
// ❌ 违反规则 1:条件内调用
if (something) {
const [v, setV] = useState(0); // 第一个渲染调用了,下次可能跳过
}
// ✅ 正确:条件放在 Hook 内部
const [v, setV] = useState(0);
if (something) {
// 使用 v
}内置 Hooks 全景图
| 类别 | Hooks | 用途 |
|---|---|---|
| 状态 | useState, useReducer |
管理组件内部状态 |
| 副作用 | useEffect, useLayoutEffect, useInsertionEffect |
同步到外部系统 |
| 引用 | useRef, useImperativeHandle |
持有可变值、暴露子组件方法 |
| 上下文 | useContext |
消费 Context |
| 性能 | useMemo, useCallback |
缓存计算结果、缓存函数引用 |
| 过渡 | useTransition, useDeferredValue |
标记非紧急更新 |
| 工具 | useId, useDebugValue, useSyncExternalStore, useOptimistic |
ID 生成、调试、外部 store 等 |
| 组合 | 自定义 Hook | 提取可复用逻辑 |
useState
const [state, setState] = useState(initialValue);初始化函数(惰性初始化)
// ❌ 每次渲染都执行 expensive(),初始值浪费
const [data, setData] = useState(expensiveComputation());
// ✅ 只执行一次
const [data, setData] = useState(() => expensiveComputation());函数式更新
// ❌ 依赖闭包中的旧值
setCount(count + 1); // 连续调用可能只加一次
// ✅ 基于最新值
setCount(c => c + 1); // 安全,三次调用会加三次🚨 陷阱:
setState是异步的。调用后立即读取 state 仍然是旧值。如需在更新后执行操作,用useEffect。
useEffect
useEffect(() => {
// 副作用逻辑(Setup)
const subscription = api.subscribe(prodId);
return () => {
// 清理函数(Cleanup)
subscription.unsubscribe();
};
}, [prodId]); // 依赖数组三种使用模式
// 1. 每次渲染后执行(无依赖数组)
useEffect(() => { ... });
// 2. 仅挂载时执行一次(空数组)
useEffect(() => {
fetchData();
}, []);
// 3. 依赖变化时执行
useEffect(() => {
fetchProduct(productId);
}, [productId]);清理函数的执行时机
组件首次渲染 → 执行 Setup
组件重新渲染 → 执行 Cleanup(用旧依赖)→ 执行 Setup(用新依赖)
组件卸载 → 执行 Cleanup🚨 陷阱:Strict Mode 下,useEffect 的 Setup 会被调用两次(Setup → Cleanup → Setup),这是有意为之,帮助发现缺少清理的问题。
useEffect vs 直接放在组件体中
| 组件体 | useEffect | |
|---|---|---|
| 执行阶段 | Render 阶段 | Commit 阶段 |
| 时机 | 可被 React 任意调用 | DOM 已更新 |
| 用途 | 计算 JSX | 副作用 |
useLayoutEffect
useLayoutEffect(() => {
// 在浏览器重绘前同步执行
measureTooltipSize();
}, [content]);与 useEffect 的区别:
| useLayoutEffect | useEffect | |
|---|---|---|
| 执行时机 | DOM 变更后,浏览器重绘前 | 浏览器重绘后 |
| 执行方式 | 同步阻塞 | 异步 |
| 适用场景 | 测量 DOM、修复视觉闪烁 | 数据获取、订阅 |
💡 最佳实践:90% 的情况用
useEffect。只有当你测量 DOM、需要防止闪烁(如 tooltip 定位)时才用useLayoutEffect。
useRef
const ref = useRef(initialValue);
// ref.current 可直接读写,修改不触发重渲染三种典型用途
// 1. DOM 引用
function InputFocus() {
const inputRef = useRef(null);
useEffect(() => { inputRef.current?.focus(); }, []);
return <input ref={inputRef} />;
}
// 2. 保存可变值(不触发渲染)
function Timer() {
const intervalRef = useRef(null);
const start = () => {
intervalRef.current = setInterval(() => console.log('tick'), 1000);
};
const stop = () => {
clearInterval(intervalRef.current);
};
}
// 3. 记录上一次的值
function usePrevious(value) {
const ref = useRef();
useEffect(() => { ref.current = value; });
return ref.current; // 返回的是"旧值"
}useContext
const ThemeContext = createContext('light');
// Provider
function App() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={theme}>
<Page />
</ThemeContext.Provider>
);
}
// Consumer
function Page() {
const theme = useContext(ThemeContext);
return <div className={theme}>当前主题:{theme}</div>;
}🚨 陷阱:Context value 变化会导致所有消费该 Context 的组件重渲染,即使它们只用了 value 的一小部分。解决方案:拆分 Context 或使用状态管理库(如 Zustand)。
useMemo & useCallback
// useMemo:缓存计算结果
const sorted = useMemo(() => {
return data.sort((a, b) => a.name.localeCompare(b.name));
}, [data]);
// useCallback:缓存函数引用
const handleClick = useCallback((id) => {
setSelected(id);
}, []);何时使用(抉择指南)
| 场景 | 不需要 | 需要 |
|---|---|---|
| 计算很简单 | `< 1ms | 复杂计算(filter/map/sort 大数组) |
| 子组件 | 原生元素 | memo(WrappedComponent) |
| 函数作为 Effect 依赖 | 普通函数 | 稳定引用避免无限 effect |
🚨 陷阱:不要无差别地对所有函数和值使用 useMemo/useCallback。它们自己也有开销(缓存存储、依赖比较)。只有在性能确实出现问题时再用。
useReducer
const initialState = { count: 0 };
function reducer(state, action) {
switch (action.type) {
case 'increment': return { count: state.count + 1 };
case 'decrement': return { count: state.count - 1 };
default: return state;
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return <button onClick={() => dispatch({ type: 'increment' })}>
{state.count}
</button>;
}| useState | useReducer |
|---|---|
| 简单值/对象 | 复杂状态逻辑 |
| 更新互相独立 | 更新互相关联 |
| 1-3 个状态变量 | 多个状态变量需协同管理 |
💡 最佳实践:当一个
setState依赖另一个 state 的值时,考虑改用useReducer。
useTransition(React 18+)
const [isPending, startTransition] = useTransition();
function handleSearch(input) {
setInputValue(input); // 紧急:立刻更新输入框
startTransition(() => {
setSearchResults(filter(input)); // 非紧急:可被中断
});
}
// isPending 为 true 时显示 loading💡 最佳实践:将大量状态的更新包裹在
startTransition中,保证用户交互(如输入、点击)不卡顿。
自定义 Hook
// 提取可复用逻辑的标准方式
function useFetch(url) {
const [data, setData] = useState(null);
const [error, setError] = useState(null);
const [loading, setLoading] = useState(true);
useEffect(() => {
let cancelled = false;
setLoading(true);
fetch(url)
.then(r => r.json())
.then(d => { if (!cancelled) setData(d); })
.catch(e => { if (!cancelled) setError(e); })
.finally(() => { if (!cancelled) setLoading(false); });
return () => { cancelled = true; };
}, [url]);
return { data, error, loading };
}命名约定
- Hook 名以
use开头(ESLint 插件依赖此约定做规则检查) - 返回值用对象(而非数组),便于按需解构
Hooks 调用链路
组件渲染
│
▼
调用组件函数
│
▼
React 传入当前 Fiber 的 dispatcher
│
▼
useState → resolveDispatcher().useState()
│
▼
从 Fiber.memoizedState 链表读/写对应槽位🔬 深入原理:React 为每个渲染周期设置一个
dispatcher。这就是为什么在组件外调用 Hook 会报 “Invalid hook call” —— dispatcher 未设置。
🚨 常见陷阱
- 在条件/循环中调用 Hook — 破坏链表顺序
- useEffect 缺少依赖 — 导致闭包过时,访问到旧的 props/state
- useEffect 死循环 — 设置 state 却没正确设置依赖
- 忘记清理 — 定时器、订阅、请求未取消
- ref 用作依赖 —
ref.current变化不会触发 effect - 滥用 useMemo/useCallback — 简单场景不需要
- 在 useEffect 中调用 setState 无防抖 — 可能导致 render → effect → render 循环