React Hooks 面试专题深度解析
从面试角度深度解析 React Hooks,包含原理、心智模型、常见坑和高频问答。
[!info] related notes
- 所属 MOC: React MOC
- 概念入口: react-hooks, react-hooks-overview
- 相关主线: react-state-model-vs-vue3-reactivity-model, react-performance-overview, react-state-management-selection
- 原子概念: react-use-state, react-use-effect, react-use-layout-effect, react-use-ref, react-use-context, react-use-reducer, react-use-memo, react-use-callback
- 索引入口: react-interview-high-frequency
React Hooks 面试专题深度解析
面试中问到 Hooks 时,不要只背 API,要展示你对「Hook 为什么这么设计」的理解。
一、必答题:Hooks 解决了什么
作为被面试者,首先要能清晰回答 Hooks 出现的背景:
类组件的三个问题
- 逻辑分散:同一件事分裂在
componentDidMount、componentDidUpdate、componentWillUnmount里 - 复用困难:想复用逻辑只能靠 HOC、render props,嵌套很深
- 函数组件能力弱:函数组件原本不能直接持有 state 和副作用
Hooks 的核心价值
把「和某个功能相关的逻辑」聚合起来,而不是按生命周期切碎。
进一步理解:react-hooks
二、必答题:Hook 的调用顺序机制
这是 Hooks 最重要的原理,面试必问。
React 怎么知道哪个 state 属于哪个 Hook
function Counter() {
const [count, setCount] = useState(0);
const [name, setName] = useState('A');
}
答案:React 依赖 Hook 的调用顺序,而不是变量名。
React 内部更接近这样理解:
- 第 1 个 Hook 槽位:
count - 第 2 个 Hook 槽位:
name
为什么不能写在条件、循环里
function Bad({ visible }) {
if (visible) {
useEffect(() => {}, []); // 错!
}
const [count, setCount] = useState(0);
}
当 visible=true 时,useEffect 是第 1 个 Hook
当 visible=false 时,useState 变成第 1 个 Hook
调用顺序一乱,React 就把状态对错位置。
进一步理解:react-use-state
三、useState 高频面试题
1. 为什么不能直接 count = count + 1
因为组件函数里的变量只是当前这次执行的局部变量。React 不会因为你改了普通变量就重新渲染,也不会把它保存到下一次渲染里。
2. 函数式更新为什么重要
setCount(c => c + 1); // 推荐
setCount(count + 1); // 不推荐
函数式更新拿到的是最新状态,不容易被闭包里旧值坑到。
3. 惰性初始化
const [value, setValue] = useState(() => expensiveInit());
只会在初始渲染时计算一次。
进一步理解:react-use-state
四、useEffect 高频面试题
1. useEffect 的本质是什么
必答题:React 官方明确说,useEffect 是「让组件与外部系统同步」。
不是:
- 「组件渲染完一定要执行的地方」
- 「代替所有事件处理器的地方」
- 「处理纯同步逻辑的地方」
2. 依赖数组的三种写法
| 写法 | 什么时候运行 |
|---|---|
useEffect(fn) | 每次渲染后 |
useEffect(fn, []) | 只在初次挂载后 |
useEffect(fn, [count]) | 首次 + count 变化时 |
3. 为什么开发环境下 Effect 执行两遍
React 官方:在严格模式下,开发环境会额外执行一次 setup + cleanup,这是压力测试,用来帮助你发现 cleanup 是否写对。
这不是 bug,是 Feature。
4. 常见坑:闭包问题
useEffect(() => {
const id = setInterval(() => {
console.log(count); // 永远打印 0
}, 1000);
return () => clearInterval(id);
}, []); // count 没在依赖里!
解决方法:
- 把依赖写对:
useEffect(..., [count]) - 用 ref 保存最新值
5. 什么时候不用 Effect
- 派生状态可以直接在渲染时计算
- 用户事件应该放在事件处理器里
五、useRef 高频面试题
useRef 和 useState 的区别
state变了会触发重渲染ref.current变了不会触发重渲染
useRef 的两个用途
- 引用 DOM:
inputRef.current.focus() - 保存不触发渲染的可变值:timer id、上一次的值、第三方实例
进一步理解:react-use-ref
六、useMemo 和 useCallback 高频面试题
什么时候该用 useMemo
- 计算真的昂贵(大数组过滤、复杂排序)
- 需要「引用稳定」传给 memo 子组件
什么时候不该用
- 不要把 useMemo 当作「到处加一点更高级」
- 缓存也有成本:依赖比较、额外心智负担
useCallback 和 useMemo 的关系
const fn = useCallback(callback, deps);
// 等于
const fn = useMemo(() => callback, deps);
进一步理解:react-use-memo, react-use-callback
七、自定义 Hook 面试题
自定义 Hook 是什么
把一段可复用的「状态 + 副作用 + 逻辑组合」提取成函数。
function useDebounce(value, delay) {
const [debounced, setDebounced] = useState(value);
useEffect(() => {
const id = setTimeout(() => setDebounced(value), delay);
return () => clearTimeout(id);
}, [value, delay]);
return debounced;
}
它不是共享状态实例
每次调用自定义 Hook,都是独立的一套 Hook 状态槽位。自定义 Hook 共享的是逻辑,不是状态实例。
进一步理解:react-custom-hooks
八、最常见的 9 个坑
| 坑 | 错误写法 | 正确写法 |
|---|---|---|
| 条件调用 Hook | if (ok) useEffect(...) | 必须顶层调用 |
| 依赖少写 | useEffect(fn, []) 少写了 userId | 依赖要反映真实引用 |
| 依赖乱写 | 故意少写来「优化」 | 依赖必须完整 |
| Effect 做派生状态 | useEffect(() => setFiltered(...)) | 直接渲染时计算 |
| Effect 响应用户事件 | useEffect(() => { if (submitted) ... }) | 放在事件处理器里 |
| 把 ref 当 state | ref.current++ 期望刷新 UI | 用 state |
| 把 state 当 ref | 想存 timer id 但用 state | 用 ref |
| 过度使用缓存 | 到处 useMemo/useCallback | 只在必要时用 |
| 忘记 cleanup | 定时器/订阅没清理 | Effect 必须 return cleanup |
九、选择 Hook 的思维框架
面试时被问到「这个场景用什么 Hook」,按这个顺序想:
- 需要让 UI 记住某个值? →
useState/useReducer - 要和外部系统同步? →
useEffect/useLayoutEffect - 只想保存不触发渲染的值? →
useRef - 跨层传数据? →
useContext - 有昂贵计算? →
useMemo - 需要稳定函数引用? →
useCallback - 想复用一段逻辑? → 自定义 Hook
十、面试加分回答
加分项 1:提到 React Compiler
React 官方现在也在强调编译器自动记忆化能力,
useCallback文档提到 React Compiler 会自动对值和函数做记忆化,从而减少手动调用useCallback的需求。
加分项 2:提到闭包是 JavaScript 特性
闭包不是 React 特有问题,但在 Hooks 里会特别明显,因为组件每次渲染都会形成新闭包。
加分项 3:展示正确的数据流思维
最有效的优化经常不是「缓存更多」,而是「少建模错误的状态」。
十一、面试答题模板
如果被问到某个 Hook,不想答散,可以按这个顺序组织:
- 这个 Hook 要解决什么问题
- 它和相邻 Hook 的边界是什么
- 它最常见的坑是什么
- 它在真实项目里通常用在哪
例如被问 useRef,不要只答“拿 DOM”,而要补:
- 它也能保存不触发渲染的可变值
- 它和 state 的边界是什么
- 它为什么常用来解决闭包旧值问题
十二、最值得记住的 10 句话
- Hooks 的本质是让函数组件拥有状态和副作用能力
- React 依赖 Hook 的调用顺序来匹配状态槽位
- 所以 Hook 只能在顶层调用,不能写进条件和循环
useState用来驱动 UIuseRef用来保存不触发渲染的可变值useEffect是「与外部系统同步」,不是「渲染后随便写逻辑」- 依赖数组必须反映真实依赖,不要靠省略依赖来「优化」
- 闭包不是 React 特有问题,但在 Hooks 里会特别明显
useMemo缓存值,useCallback缓存函数引用- 自定义 Hook 复用的是逻辑,不是共享同一份状态