3. React Hooks 深度理解与实战
本章聚焦面试中高频出现的 Hooks 陷阱、自定义 Hook 设计能力及追问应对。掌握这些内容,能体现你对 React 函数式编程、闭包、副作用管理的真实理解,而非仅仅停留在“会用”层面。
3.1 问答卡片:useState 陷阱 — 闭包与过期状态
面试问题
在 useState 中,为什么每隔一秒打印的 count 值总是初始值而非最新的 state?如何修复?
面试官考察点
- 是否理解闭包在 React 渲染中的行为。
- 是否知道函数式更新 setState(prev => ...) 的原理。
- 能否正确使用 useRef 或 useCallback 治理过期依赖。
回答框架
- 定位问题:setInterval 或 Promise 回调用到了旧闭包中捕获的 count 值。
- 解释原因:React 每次渲染产生独立闭包,setInterval 只注册一次所以用到初始闭包。
- 给出解法:使用函数式更新
setCount(c => c + 1)或通过 useRef 存取最新值。
示例回答(80字)
因为 setInterval 回调捕获了第一次渲染时的 count 值(闭包),后续渲染不会更新该闭包。使用 setCount(c => c + 1) 可从 state 队列获取最新值;或用 useRef 保存最新 count 实现读写分离。
可能追问
- 用 useRef 解决时,为什么不需要添加到 useEffect 的 deps 中?
- 函数式更新与直接传值有什么区别?
- 如果连续执行三次 setCount(count + 1) 结果是什么?为什么?
避坑提醒
不要一上来就说“用 useRef 保存回调”,面试官更希望你优先想到函数式更新——这是 React 官方的意图。只有需要在回调中读取最新值而非设置值时才使用 ref。
3.2 问答卡片:useEffect 陷阱 — 依赖数组与清除函数
面试问题
useEffect 的依赖数组(deps)中应该包含哪些变量?如果误含/漏含,会造成什么后果?
面试官考察点
- 是否理解 deps 是“数据契约”——告诉 React 何时重新执行副作用。
- 能否区分“语义依赖”与“运行依赖”。
- 是否熟悉 eslint-plugin-react-hooks 的规则及原理。
回答框架
- 原则:effect 内部所有引用的外部变量(props、state、派生值)都必须加入 deps。
- 漏含后果:闭包捕获旧值,导致 bug 或无限循环(漏了函数时)。
- 滥用后果:频繁重复执行副作用,性能下降或死循环。
- 工具保障:使用 exhaustive-deps 规则自动检查。
示例回答(90字)
useEffect 必须将 effect 内部引用的所有响应式变量(state、props、派生值)加入 deps。漏含会导致闭包捕获旧值(如定时器读取旧 count);多包含(如函数未包裹 useCallback)则触发不必要执行。使用 eslint 插件 exhaustive-deps 避免遗漏。
可能追问
- 如果我想在 effect 里读取最新 state 但不放在 deps 中,有什么办法?
- useEffect 的清除函数执行时机?如何防止内存泄漏?
- useLayoutEffect 与 useEffect 的区别?
避坑提醒
不要在 deps 中放“引用类型”(对象/数组)而不做 memo,否则每次渲染都会导致 deps 变化。优先依赖基本类型,必要时使用 useMemo / useCallback 稳定引用。
3.3 问答卡片:自定义 Hook 封装 — 设计与约束
面试问题
设计一个自定义 Hook 时,需要注意哪些原则?如何确保它具有良好的可复用性和可测试性?
面试官考察点
- 是否理解 Hook 本质是函数的组合,而非继承。
- 能否区分“状态逻辑”与“UI 逻辑”。
- 是否考虑过自定义 Hook 的边界情况(参数校验、卸载处理)。
回答框架
- 命名与职责:以 use 开头,单一职责(一个 Hook 解决一个问题)。
- 入参设计:参数应尽量保持原始值,避免依赖对象型引用。
- 返回值稳定:使用 useMemo / useCallback 包装返回的函数或对象,减少不必要渲染。
- 可测试性:利用 React Testing Library 的 renderHook 隔离测试。
示例回答(100字)
自定义 Hook 应遵循:① 以 use 开头,命名描述功能;② 抽离纯状态逻辑,不包含 JSX;③ 参数尽量用基本类型或通过 useMemo 稳定;④ 返回对象用 useMemo 缓存引用;⑤ 提供清理函数(如 setTimeout 清除);⑥ 通过 renderHook 测试不同入参下的返回值及副作用。
可能追问
- 如何确保自定义 Hook 在 StrictMode 下不会产生双重副作用?
- 自定义 Hook 能否嵌套使用?对依赖链有何影响?
- 如果两个组件共享同一个 Hook 实例,如何设计状态同步?
避坑提醒
不要将组件内部的 ref 或 DOM 操作直接暴露给外部。应该通过回调封装,保持 Hook 的“渲染无关性”。始终假设 Hook 可能被多次调用且各实例独立。
3.4 项目案例:数据列表的无限滚动 + 防抖搜索
| 项目名称 | 高性能商品搜索列表(React + TypeScript) |
| 业务背景 | 电商后台管理系统中,需要提供搜索框实时过滤、滚动加载更多数据,且要求避免网络请求过频。 |
| 规模指标 | 列表项约 10 万条;用户搜索操作 100+ 次/分钟;首屏加载时间 < 2s,滚动时无卡顿。 |
| 技术栈 | React 18 + TypeScript + React-Router v6 + Redux Toolkit(或 useReducer)+ IntersectionObserver + 自由封装 Hook |
| 核心难点 |
|
| 面试高频追问 |
|
| 可迁移回答话术 |
面试中强调“职责分离”:防抖逻辑封装在 Hook 内部,组件只关心调用 |