Hooks 核心与常见坑
面试里 Hooks 很少考「API 列表」,更多考:规则为什么存在、闭包为什么陈旧、依赖数组怎么想。
一句话定位
Hooks 让函数组件也能有状态与副作用;本质是 React 用 调用顺序 把 hook 状态挂到当前 Fiber 上。
规则:只能顶层、同序调用
// ❌ 条件 / 循环里调用
if (cond) {
const [x, setX] = useState(0)
}
// ✅ 永远同一顺序
const [x, setX] = useState(0)
if (cond) {
// 用 x,不要在这里「决定要不要 useState」
}
面试怎么答:
- React 靠 调用次序 对应 hook 链表节点,不是靠名字。
- 条件调用会让前后两次渲染对不上,状态错乱。
eslint-plugin-react-hooks的rules-of-hooks就是在保这个不变量。
useState:更新是异步合并的
const [n, setN] = useState(0)
function onClick() {
setN(n + 1)
setN(n + 1) // 两次都基于同一个 n → 只 +1
setN((prev) => prev + 1) // 函数式更新:基于队列里的最新值
}
要点:
- 需要「基于上一次结果」→ 用 函数式更新
- 对象/数组要当 immutable 换新引用(除非你用 immer 等)
- 初始值昂贵时用惰性初始化:
useState(() => createHeavy())
useEffect:同步外部系统,不是「代替生命周期」
常见模型:
| 场景 | 依赖 | 注意 |
|---|---|---|
| 挂载时拉一次数据 | [] | 仍要处理卸载 / 竞态 |
| 随 id 变化重拉 | [id] | 清理上一次请求 |
| 订阅事件 / 定时器 | 对应依赖 | 必须 cleanup |
useEffect(() => {
let cancelled = false
fetchUser(id).then((u) => {
if (!cancelled) setUser(u)
})
return () => {
cancelled = true // 或 AbortController.abort()
}
}, [id])
别把 effect 当「渲染后必跑的业务逻辑中枢」。 能在事件里做的(点击后请求)优先放事件;effect 更适合「让 React 状态与外部(DOM、订阅、非 React 部件)对齐」。
依赖数组心智模型
- 缺依赖 → 闭包读到旧值(陈旧闭包)
- 多依赖 / 每次新引用 → 无限循环或多余执行
- 对象、函数作依赖时,先问:能不能下沉、拆字段、或用
useCallback/ 稳定 ref
陈旧闭包(高频)
function Timer() {
const [count, setCount] = useState(0)
useEffect(() => {
const id = setInterval(() => {
// ❌ 一直看到 mount 时的 count(若 deps 为 [])
setCount(count + 1)
}, 1000)
return () => clearInterval(id)
}, []) // 问题在这
// ✅ 写法一:函数式更新
// setCount(c => c + 1)
// ✅ 写法二:deps 纳入 count(会反复清计时器,通常不如函数式)
}
另一套路:用 ref 存「最新回调 / 最新值」,定时器只建一次:
const countRef = useRef(count)
countRef.current = count
useEffect(() => {
const id = setInterval(() => {
console.log(countRef.current)
}, 1000)
return () => clearInterval(id)
}, [])
useRef:不变的盒子
ref.current变了 不会 触发重渲染- 典型用途:DOM 节点、定时器 id、上一次 props、保存最新回调
const inputRef = useRef<HTMLInputElement>(null)
useEffect(() => {
inputRef.current?.focus()
}, [])
和 useState 对比:要驱动 UI → state;只要「跨渲染记住一个可变槽」→ ref。
useMemo / useCallback:默认别用
const filtered = useMemo(() => list.filter(...), [list, keyword])
const onSelect = useCallback((id: string) => {...}, [deps])
面试稳妥答法:
- 先保证「别让父组件无意义重渲染子树」和「选择器 / 状态拆分」
useMemo:贵计算,或要保持 引用稳定 交给依赖引用相等的子组件 / effectuseCallback:同理,主要是稳定函数引用- 乱加反而多一次 hook 开销 + 依赖心智负担;React Compiler 普及后,很多手写 memo 会变少
自定义 Hook:复用「状态逻辑」不是复用 UI
约定:名字 useXxx,内部可调其他 hooks,遵守同样规则。
function useDebouncedValue<T>(value: T, delay = 300) {
const [debounced, setDebounced] = useState(value)
useEffect(() => {
const t = setTimeout(() => setDebounced(value), delay)
return () => clearTimeout(t)
}, [value, delay])
return debounced
}
面试常问:「自定义 Hook 和普通函数区别?」→ 普通函数不能调 Hooks;自定义 Hook 把多组件重复的状态+副作用封装掉,UI 仍各自写。
面试速答清单
- 为什么不能条件调用 Hooks? → 调用序对应内部链表。
- setState 两次为什么只加一次? → 批处理 + 同一次渲染快照;用函数式更新。
- useEffect 和 useLayoutEffect? → 后者在 paint 前同步跑,会阻塞绘制;测 DOM 布局、防闪烁才用。
- 如何避免请求竞态? → cleanup 里
cancelled/AbortController。 - useMemo 一定更快吗? → 不一定;先测量,再针对贵计算或引用稳定。
下一篇:渲染机制与性能优化