Skip to main content

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-hooksrules-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])

面试稳妥答法:

  1. 先保证「别让父组件无意义重渲染子树」和「选择器 / 状态拆分」
  2. useMemo:贵计算,或要保持 引用稳定 交给依赖引用相等的子组件 / effect
  3. useCallback:同理,主要是稳定函数引用
  4. 乱加反而多一次 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 仍各自写。

面试速答清单

  1. 为什么不能条件调用 Hooks? → 调用序对应内部链表。
  2. setState 两次为什么只加一次? → 批处理 + 同一次渲染快照;用函数式更新。
  3. useEffect 和 useLayoutEffect? → 后者在 paint 前同步跑,会阻塞绘制;测 DOM 布局、防闪烁才用。
  4. 如何避免请求竞态? → cleanup 里 cancelled / AbortController
  5. useMemo 一定更快吗? → 不一定;先测量,再针对贵计算或引用稳定。

下一篇:渲染机制与性能优化