React Hooks 全解析(原理+场景+面试)
文档说明
本文覆盖 React 所有内置 Hooks(含 React 18+ 新增),从定义&原理、使用语法、应用场景、面试常问点四个维度拆解,同时补充 Hooks 通用规则、性能优化、面试高频综合问题,适合开发参考与面试备考。
一、Hooks 核心概述
1. 什么是 Hooks?
React 16.8 新增特性,允许函数组件使用状态、生命周期、上下文等 React 核心特性,替代类组件,解决类组件的逻辑复用难、this 指向混乱、生命周期耦合等问题。
2. Hooks 核心设计理念
-
函数组件无实例,通过 Hooks 关联 React 内部状态;
-
每个 Hooks 调用对应 React 内部的「Hook 节点」,按调用顺序挂载到组件对应的 Fiber 节点的 Hook 链表中;
-
状态存储在 Fiber 节点中,组件重新渲染时按顺序读取 Hook 链表,保证状态与 Hooks 一一对应。
3. Hooks 使用规则(面试必问)
-
只能在函数组件/自定义 Hooks 顶层调用(不能在条件、循环、嵌套函数中)→ 避免 Hook 链表错位;
-
只能在React 函数组件/自定义 Hooks 中调用(不能在普通 JS 函数中)→ 无 Fiber 关联无法存储状态。
二、基础 Hooks(React 核心)
1. useState —— 响应式状态管理
定义&原理
让函数组件拥有响应式状态的核心 Hook,返回「状态值 + 更新状态的函数」。
-
首次渲染:React 创建 Hook 节点,存储初始状态,返回
[初始值, 更新函数]; -
状态更新:调用更新函数时,React 将新状态加入更新队列,触发组件重渲染;
-
核心特性:更新函数是异步批量执行(合成事件/钩子中),状态更新是「替换」而非「合并」(区别于类组件
setState)。
语法
// 基础用法
const [count, setCount] = useState(0);
// 函数式初始值(初始值计算昂贵时,仅首次渲染执行)
const [data, setData] = useState(() => {
return 计算昂贵的初始值;
});
// 函数式更新(依赖旧状态)
setCount(prevCount => prevCount + 1);
// 对象/数组更新(必须替换,而非修改)
const [user, setUser] = useState({ name: '张三', age: 20 });
setUser(prev => ({ ...prev, age: prev.age + 1 }));
应用场景
-
组件内部基础状态管理(输入框值、开关状态、列表数据);
-
简单交互状态(弹窗显隐、加载状态、分页页码);
-
需基于旧状态更新的场景(如计数器、列表增删)。
面试常问点
- Q1:为什么
useState更新函数是异步的?
✅ 回答:React 为优化性能,在合成事件(如 onClick)、生命周期钩子中批量执行状态更新,避免频繁重渲染;原生事件(如 addEventListener)、setTimeout 中同步执行。
- Q2:初始值只执行一次的原因?
✅ 回答:首次渲染时计算并存储,后续渲染跳过,减少不必要的计算开销。
- Q3:状态更新是替换而非合并,如何处理对象/数组?
✅ 回答:用展开运算符(...)创建新对象/数组,如 setUser(prev => ({ ...prev, age: 30 }))。
- Q4:多次调用
useState,最终状态如何?
✅ 回答:批量更新时合并,取最后一次计算结果(如 setCount(n=>n+1); setCount(n=>n+1) 最终 count+2);非批量时即时更新。
- Q5:如何解决
useState的闭包陷阱?
✅ 回答:用函数式更新(setCount(prev => prev + 1)),或 useRef 保存最新值。
- Q6:为什么不能直接修改 state(如
state.count = 1)?
✅ 回答:React 依赖状态的引用变化触发重渲染,直接修改不会触发更新,且违反不可变原则。
2. useEffect —— 副作用管理
定义&原理
模拟函数组件的生命周期,处理「副作用」(数据请求、DOM 操作、订阅/取消订阅等)。
-
原理:
-
基于 React 的「副作用队列」,执行时机分三类:
-
依赖项为空
[]:仅组件挂载后执行,卸载前执行清理函数; -
依赖项有值
[a, b]:挂载后 + 依赖项变化后执行; -
无依赖项:每次渲染后执行;
-
-
默认在浏览器绘制完成后异步执行(不阻塞渲染),对比
useLayoutEffect(绘制前同步执行)。
-
-
执行时机:
-
首次渲染后执行(对应
componentDidMount); -
依赖项变化后执行(对应
componentDidUpdate); -
组件卸载前执行清理函数(对应
componentWillUnmount);
-
-
核心逻辑:每次渲染完成后,先执行上一次的清理函数(如有),再执行本次 effect 逻辑。
语法
useEffect(() => {
// 副作用逻辑(如请求数据、定时器)
const timer = setInterval(() => setCount(c => c+1), 1000);
// 清理函数(组件卸载/依赖变化时执行)
return () => {
clearInterval(timer);
};
}, [dependencies]); // 依赖项数组:空数组=仅首次执行;无数组=每次执行;指定依赖=依赖变化执行
应用场景
-
数据请求(需注意依赖项,避免无限循环);
-
DOM 操作(如设置页面标题、滚动到指定位置);
-
订阅/取消订阅(
WebSocket、事件监听、Redux订阅); -
定时器/延时器的创建与清理;
-
监听 props/state 变化执行联动逻辑。
面试常问点
- Q1:
useEffect执行时机?
✅ 回答:首次渲染后、依赖变化后,在浏览器绘制完成后异步执行,不阻塞渲染。
- Q2:依赖项数组为空的含义?
✅ 回答:仅首次渲染执行,模拟类组件 componentDidMount。
- Q3:为什么会出现无限循环?
✅ 回答:依赖项未正确设置(如依赖项是对象/数组,每次渲染创建新引用,导致 effect 重复执行);或在 effect 中修改依赖项。
- Q4:如何取消
useEffect中的数据请求?
✅ 回答:使用 AbortController 中断请求:
useEffect(() => {
const controller = new AbortController();
fetch('/api/data', { signal: controller.signal })
.then(res => res.json())
.then(data => setData(data));
return () => controller.abort(); // 清理时中断请求
}, []);
useEffect中修改state会导致无限循环吗?
若 state 不在依赖项中:不会触发 effect 重执行;若在依赖项中:需确保修改逻辑收敛(如加条件判断)。
3. useContext —— 跨组件状态共享
定义&原理
跨组件共享状态的 Hook,替代 Props 层层传递(Context API 的函数组件版本)。
-
原理:
React.createContext创建上下文对象 →Context.Provider包裹组件树并传递value→useContext读取当前上下文的 value; -
核心特性:
Provider的value变化时,所有使用useContext的组件都会重渲染(即使仅使用部分 value)。
语法
// 1. 创建 Context(默认值仅无 Provider 时生效)
const ThemeContext = React.createContext('light');
// 2. 提供 Context(父组件)
function Parent() {
const [theme, setTheme] = useState('dark');
return (
<ThemeContext.Provider value={{ theme, setTheme }}>
<Child />
</ThemeContext.Provider>
);
}
// 3. 消费 Context(子组件)
function Child() {
const { theme, setTheme } = useContext(ThemeContext);
return <button onClick={() => setTheme('light')}>{theme}</button>;
}
应用场景
-
全局状态共享(用户登录状态、主题、语言配置);
-
跨多层组件传递数据(避免 Props 钻透);
-
组件库的上下文配置(如 UI 组件的尺寸、颜色)。
面试常问点
- Q1:useContext 组件何时重渲染?
✅ 回答:Provider 的 value 变化时(浅比较),无论组件是否使用该 value,都会重渲染。
-
Q2:如何优化 useContext 导致的不必要重渲染?
-
拆分细粒度 Context(如主题 Context、用户 Context 分开);
-
配合 React.memo 包装子组件;
-
将不变的状态抽离(如方法绑定到父组件,避免 value 频繁变化)。
-
-
Q3:Context 默认值什么时候生效?
✅ 回答:组件没有匹配的 Provider 时生效,而非 Provider 的 value 为 undefined 时。
- Q4:useContext 和 Redux 的区别?
✅ 回答:
| 维度 | useContext | Redux |
|---|---|---|
| 定位 | 跨组件状态共享 | 全局状态管理 |
| 中间件支持 | 无 | 支持(redux-thunk/saga) |
| 调试 | 无原生 DevTools | 支持时间旅行、DevTools |
| 适用场景 | 简单跨组件共享 | 复杂全局状态(如电商购物车) |
三、额外 Hooks(扩展能力)
1. useRef —— 持久化非响应式值
定义&原理
创建可变的 ref 对象,其 .current 属性可存储任意值,值变化不触发组件重渲染。
-
原理:ref 对象在组件整个生命周期中保持同一引用,首次渲染创建,后续渲染复用;
-
两类用途:DOM ref(绑定 DOM 元素)、普通值 ref(存储临时/持久化数据)。
语法
// 1. DOM ref(获取输入框焦点)
const inputRef = useRef(null);
const focusInput = () => inputRef.current.focus();
return <input ref={inputRef} />;
// 2. 存储持久化数据(解决闭包陷阱)
const countRef = useRef(0);
useEffect(() => {
countRef.current = count; // 存储最新 count
const timer = setInterval(() => {
console.log('最新count:', countRef.current); // 闭包中访问最新值
}, 1000);
return () => clearInterval(timer);
}, [count]);
应用场景
-
获取 DOM 元素/组件实例(输入框焦点、滚动元素、canvas 绘图);
-
存储无需触发渲染的持久化数据(定时器 ID、上一次的状态值);
-
解决
Hooks闭包陷阱(在闭包中访问最新状态)。
面试常问点
- Q1:useRef 和 createRef 的区别?
✅ 回答:createRef 每次渲染都会创建新的 ref 对象;useRef 在组件生命周期中保持同一个 ref。
- Q2:useRef 值变化为什么不触发渲染?
✅ 回答:React 未监听 ref.current 的变化,ref 仅用于存储值,不参与响应式系统。
- Q3:闭包陷阱是什么?如何用
useRef解决?
✅ 回答:场景:useEffect 中访问的 state 是首次渲染的闭包值,无法获取最新值;
解决:将最新状态存入 ref.current,在闭包中访问 ref.current。
2. useReducer —— 复杂状态管理
定义&原理
替代 useState 的复杂状态管理 Hook,基于 Redux 的 reducer 思想,适合多状态关联更新的场景。
-
核心:
state + action → newState,纯函数 reducer 处理状态更新逻辑; -
执行流程:调用
dispatch(action)→ React 执行 reducer 计算新 state → 触发组件重渲染。
语法
// 1. 定义 reducer(纯函数,无副作用)
const reducer = (state, action) => {
switch (action.type) {
case 'INCREMENT':
return { ...state, count: state.count + 1 };
case 'SET_NAME':
return { ...state, name: action.payload };
default:
throw new Error(`未知action:${action.type}`);
}
};
// 2. 使用 useReducer
function Counter() {
// 初始状态 + 惰性初始化(第三个参数)
const [state, dispatch] = useReducer(reducer, { count: 0, name: '张三' });
return (
<div>
<p>{state.name}: {state.count}</p>
<button onClick={() => dispatch({ type: 'INCREMENT' })}>+1</button>
<button onClick={() => dispatch({ type: 'SET_NAME', payload: '李四' })}>改名</button>
</div>
);
}
应用场景
-
复杂状态管理(表单多字段、购物车、登录状态);
-
多个状态关联更新(如用户信息 + 权限 + 登录状态);
-
状态逻辑需复用(抽离
reducer函数); -
小型应用替代
Redux(无需全局 store)。
面试常问点
- Q1:useReducer 和 useState 的区别?
✅ 回答:
| 维度 | useState | useReducer |
|---|---|---|
| 适用场景 | 简单独立状态 | 复杂关联状态 |
| 逻辑组织 | 状态更新逻辑散在组件中 | 集中在 reducer 函数中 |
| 复用性 | 低 | 高(reducer 可抽离复用) |
| 调试 | 直接查看 state | 通过 action 追溯状态变化 |
- Q2:reducer 为什么必须是纯函数?
✅ 回答:纯函数无副作用、输出仅由输入决定,保证状态更新可预测,便于调试和时间旅行。
- Q3:如何用 useReducer 实现状态重置?
✅ 回答:利用惰性初始化:
const init = (initialCount) => ({ count: initialCount });
const [state, dispatch] = useReducer(reducer, 0, init);
// 重置 action
case 'RESET':
return init(action.payload);
3. useCallback —— 缓存函数引用
定义&原理
缓存函数引用的 Hook,避免每次渲染创建新函数,优化子组件重渲染(需配合 React.memo)。
-
原理:依赖项不变时,返回同一个函数引用;依赖项变化时,创建新函数;
-
核心:仅优化性能,无功能上的必要性。
语法
// 缓存回调函数(依赖 count)
const handleClick = useCallback(() => {
console.log('count:', count);
}, [count]);
// 传递给子组件(需配合 React.memo)
return <Child onClick={handleClick} />;
应用场景
-
传递给子组件的回调函数(配合
React.memo避免子组件不必要重渲染); -
作为
useEffect/useMemo的依赖项(避免依赖项变化导致重复执行); -
高频渲染组件(如列表项)的事件处理函数。
面试常问点
- Q1:useCallback 的作用?
✅ 回答:缓存函数引用,减少子组件因 props 变化导致的不必要重渲染。
- Q2:为什么必须配合 React.memo 使用?
✅ 回答:若无 React.memo,子组件会因父组件重渲染而重渲染,useCallback 缓存函数无意义。
-
Q3:什么时候不需要用 useCallback?
-
函数不传递给子组件;
-
子组件未用 React.memo;
-
依赖项频繁变化(缓存开销 > 重渲染开销)。
-
4. useMemo —— 缓存计算结果
定义&原理
缓存昂贵计算结果的 Hook,避免每次渲染重复执行耗时逻辑。
-
原理:依赖项不变时,返回缓存的计算结果;依赖项变化时,重新计算;
-
注意:React 不保证缓存永久有效(低优先级时可能清理),且渲染期间执行(不可写副作用逻辑)。
语法
// 缓存昂贵计算(如大数据排序)
const sortedList = useMemo(() => {
return list.sort((a, b) => a.value - b.value);
}, [list]); // 仅 list 变化时重新排序
应用场景
-
昂贵计算(大数据排序/过滤、深拷贝、复杂数学计算);
-
缓存组件(返回 JSX,配合
React.memo优化); -
作为子组件
props(避免每次渲染传递新值导致子组件重渲染)。
面试常问点
- Q1:useMemo 和 useCallback 的区别?
✅ 回答:useMemo 缓存「计算结果」(任意类型),useCallback 缓存「函数引用」;本质都是依赖项驱动的缓存。
- Q2:useMemo 和 React.memo 的区别?
✅ 回答:useMemo 缓存计算结果/组件,React.memo 缓存组件渲染(浅比较 props)。
- Q3:能不能用 useMemo 缓存所有计算?
✅ 回答:不建议,缓存有内存开销,仅用于昂贵计算(普通计算的缓存开销 > 重计算开销)。
5. useImperativeHandle —— 自定义暴露的 ref 接口
定义&原理
自定义暴露给父组件的 ref 接口,避免父组件直接访问子组件的 DOM/实例,增强封装性。
- 原理:配合
forwardRef使用,重写ref.current,仅暴露指定方法/属性,而非整个子组件实例。
语法
// 子组件(暴露指定方法)
const Child = forwardRef((props, ref) => {
const inputRef = useRef(null);
// 自定义暴露的接口
useImperativeHandle(ref, () => ({
focus: () => inputRef.current.focus(),
clear: () => inputRef.current.value = ''
}));
return <input ref={inputRef} />;
});
// 父组件(调用子组件暴露的方法)
const parentRef = useRef(null);
const handleFocus = () => parentRef.current.focus();
return <Child ref={parentRef} />;
应用场景
-
父组件需主动控制子组件(输入框聚焦、弹窗关闭、滚动到顶部);
-
封装 UI 组件时,暴露有限接口,隐藏内部实现;
-
避免父组件直接操作子组件
DOM,降低耦合。
面试常问点
- Q1:
useImperativeHandle的作用?
✅ 回答:自定义暴露给父组件的 ref 接口,增强封装性,避免直接操作 DOM。
- Q2:为什么要避免直接访问子组件
ref?
✅ 回答:破坏组件封装性,违反 React 单向数据流,增加组件耦合。
- Q3:
useImperativeHandle必须配合forwardRef吗?
✅ 回答:是的,子组件需通过 forwardRef 接收父组件传递的 ref。
6. useLayoutEffect —— 同步 DOM 布局操作
定义&原理
与 useEffect 功能一致,但执行时机不同:DOM 更新后、浏览器绘制前同步执行,阻塞绘制。
- 执行顺序:组件渲染 → DOM 更新 →
useLayoutEffect执行 → 浏览器绘制 →useEffect执行。
语法
useLayoutEffect(() => {
// 同步读取 DOM 布局(如计算元素位置)
const rect = ref.current.getBoundingClientRect();
setTop(rect.top); // 避免布局抖动
return () => { /* 清理逻辑 */ };
}, [dependencies]);
应用场景
-
同步读取/修改 DOM 布局(如计算元素位置、调整样式避免闪烁);
-
必须在浏览器绘制前完成的 DOM 操作(如避免
useEffect导致的样式闪烁)。
面试常问点
- Q1:useLayoutEffect 和 useEffect 的核心区别?
✅ 回答:
| 维度 | useLayoutEffect | useEffect |
|---|---|---|
| 执行时机 | DOM 更新后、绘制前同步执行 | 绘制后异步执行 |
| 阻塞渲染 | 是(需减少复杂逻辑) | 否 |
| 用途 | DOM 布局操作(避免闪烁) | 副作用(请求、订阅) |
- Q2:服务端渲染(SSR)中使用 useLayoutEffect 会报错?
✅ 回答:服务端无 DOM,useLayoutEffect 会触发警告,需加环境判断:
useEffect(() => {
if (typeof window !== 'undefined') {
// 原 useLayoutEffect 逻辑
}
}, [dependencies]);
7. useDebugValue —— 自定义 Hook 调试标签
定义&原理
自定义 Hook 的调试标签,在 React DevTools 中显示自定义 Hook 的状态,增强调试体验。
- 原理:React DevTools 读取该值并显示在自定义 Hook 旁,支持函数式传入(延迟计算昂贵值)。
语法
// 自定义 Hook
function useAuth() {
const [user, setUser] = useState(null);
// 设置调试标签(DevTools 中显示)
useDebugValue(user ? '已登录' : '未登录');
// 函数式延迟计算(仅 DevTools 打开时执行)
useDebugValue(user, (u) => u ? `用户:${u.name}` : '未登录');
return { user, login: () => setUser({ name: '张三' }) };
}
应用场景
开发自定义 Hook 时(如 useRequest、usePagination),增强调试体验。
面试常问点
- Q1:useDebugValue 影响生产环境吗?
✅ 回答:不影响,生产环境会被 React 忽略。
- Q2:为什么用函数式 useDebugValue?
✅ 回答:值计算昂贵时,延迟计算(仅 DevTools 打开时执行),优化性能。
四、React 18+ 并发特性 Hooks
1. useDeferredValue —— 延迟非紧急状态更新
定义&原理
延迟更新非紧急状态,让紧急更新(如输入框)优先执行,避免卡顿。
- 原理:紧急状态更新 → 组件重渲染(
useDeferredValue返回旧值)→ 浏览器空闲 → 非紧急状态更新 → 组件再次渲染。
语法
// 输入框(紧急)+ 列表过滤(非紧急)
const [input, setInput] = useState('');
const deferredInput = useDeferredValue(input, { timeoutMs: 500 }); // 最大延迟500ms
// 延迟过滤列表(避免输入卡顿)
const filteredList = useMemo(() => {
return list.filter(item => item.includes(deferredInput));
}, [deferredInput]);
应用场景
-
输入框实时搜索(输入紧急,搜索结果非紧急);
-
大数据列表过滤/排序(避免阻塞用户交互);
-
非紧急渲染的复杂组件(如图表、富文本)。
面试常问点
- Q1:useDeferredValue 的作用?
✅ 回答:优先处理用户交互(如输入、点击),延迟非紧急状态更新,提升体验。
- Q2:useDeferredValue 和 useTransition 的区别?
✅ 回答:useDeferredValue 延迟「状态值」,useTransition 标记「更新逻辑」为非紧急。
2. useTransition —— 标记非紧急更新
定义&原理
标记非紧急状态更新为「过渡更新」,React 优先处理紧急更新,非紧急更新可被中断。
- 返回值:
[isPending, startTransition]→ isPending 标识过渡中,startTransition 包裹非紧急更新。
语法
const [isPending, startTransition] = useTransition({ timeoutMs: 500 });
const [input, setInput] = useState('');
const [list, setList] = useState([]);
const handleInput = (e) => {
// 紧急更新:输入框值
setInput(e.target.value);
// 非紧急更新:过滤列表
startTransition(() => {
setList(list.filter(item => item.includes(e.target.value)));
});
};
应用场景
-
输入框过滤大数据列表;
-
切换标签页加载大量数据;
-
任何需优先保证用户交互的非紧急更新场景。
面试常问点
- Q1:useTransition 的核心价值?
✅ 回答:允许 React 中断非紧急更新,优先处理用户交互,避免页面卡顿。
- Q2:isPending 的作用?
✅ 回答:标识过渡更新是否进行中,用于显示加载状态(如骨架屏)。
3. useId —— 生成同构唯一 ID
定义&原理
生成唯一且稳定的 ID,解决服务端渲染(SSR)中客户端/服务端 ID 不一致的问题。
- 原理:基于组件树结构生成 ID,同构渲染中保持一致,避免 hydration 警告。
语法
const id = useId();
// 生成带前缀的 ID
const inputId = useId('input-');
return (
<div>
<label htmlFor={inputId}>用户名</label>
<input id={inputId} />
</div>
);
应用场景
-
SSR 中生成唯一 ID(label 的 for 属性、表单元素 ID);
-
动态生成多个元素的唯一 ID(如列表项)。
面试常问点
- Q1:useId 和 Math.random() 的区别?
✅ 回答:Math.random() 生成的 ID 在服务端/客户端不一致,导致 SSR 警告;useId 生成的 ID 稳定且唯一。
- Q2:useId 能用于列表项的 key 吗?
✅ 回答:不建议,key 应基于数据本身(如 item.id),而非生成的 ID(破坏列表复用逻辑)。
五、高级 Hooks
1. useSyncExternalStore —— 同步外部数据源
定义&原理
同步外部数据源(如 Redux、localStorage、全局变量)到 React 组件,支持并发渲染,替代 useEffect 监听外部状态。
- 解决的问题:
useEffect监听外部状态会导致并发渲染时数据不一致,useSyncExternalStore是原子化更新。
语法
// 同步 localStorage 状态
const storedValue = useSyncExternalStore(
// 订阅函数:变化时触发重渲染
(callback) => {
window.addEventListener('storage', callback);
return () => window.removeEventListener('storage', callback);
},
// 获取当前快照
() => localStorage.getItem('key'),
// 服务端快照(可选)
() => 'default'
);
应用场景
-
集成外部状态管理库(Redux、MobX)到 React 18+;
-
监听浏览器 API(localStorage、sessionStorage)变化;
-
同步自定义全局状态到 React 组件。
面试常问点
- Q1:为什么不用 useEffect 监听外部状态?
✅ 回答:并发渲染时,useEffect 的更新可能被中断,导致数据不一致;useSyncExternalStore 是原子化更新,更可靠。
2. useInsertionEffect —— CSS-in-JS 样式插入
定义&原理
专为 CSS-in-JS 库设计的 Hook,在 DOM 生成后、useLayoutEffect 执行前插入样式,避免闪烁。
- 执行顺序:
DOM生成 →useInsertionEffect→useLayoutEffect→ 浏览器绘制。
语法
useInsertionEffect(() => {
// 插入样式到 DOM
const style = document.createElement('style');
style.innerHTML = `.btn { color: red; }`;
document.head.appendChild(style);
return () => document.head.removeChild(style);
}, []);
应用场景
-
CSS-in-JS 库的样式插入(Styled Components、Emotion);
-
动态生成样式并插入 DOM,避免闪烁。
六、Hooks 面试高频综合问题
1. 自定义 Hook 的设计原则?
-
命名以
use开头(遵循React规范,DevTools识别); -
单一职责(一个
Hook处理一类逻辑,如useRequest处理请求); -
可复用(参数化配置,如
useRequest(url, options)); -
组合
Hooks(基于内置Hooks封装,避免重复逻辑); -
可选
useDebugValue增强调试。
2. 如何解决 Hooks 闭包陷阱?
-
场景:
useEffect中访问的state是首次渲染的闭包值,无法获取最新值; -
解决方案:
-
用
useRef存储最新状态,在闭包中访问ref.current; -
依赖项中加入该
state(触发useEffect重新执行); -
使用
useReducer,通过dispatch获取最新状态。
-
3. Hooks 对比类组件的优势?
-
逻辑复用更简单(自定义 Hooks 替代高阶组件/Render Props);
-
代码更简洁(函数式写法,无 this 绑定、生命周期耦合);
-
支持并发渲染(React 18+ Hooks 适配);
-
无 this 指向混乱问题。
4. 如何优化 Hooks 组件性能?
-
减少不必要重渲染:
-
React.memo包装组件; -
useCallback缓存函数,useMemo缓存计算结果;
-
-
优化副作用:
-
正确设置
useEffect依赖项; -
清理副作用(定时器、订阅);
-
-
并发优化:useTransition/useDeferredValue 处理非紧急更新;
-
拆分组件:将重渲染频繁的部分拆分为独立组件。
5. 常见 Hooks 错误用法?
-
错误1:useState 更新对象时直接修改(
setUser(user.age=20))→ 解决方案:展开运算符创建新对象; -
错误2:useEffect 依赖项缺失 → 解决方案:用 eslint-plugin-react-hooks 检查,完整设置依赖项;
-
错误3:条件中调用 Hooks → 解决方案:将条件逻辑移到 Hooks 内部;
-
错误4:滥用
useCallback/useMemo→ 解决方案:仅用于昂贵计算/传递给子组件的函数; -
错误5:useRef 存储响应式状态 → 解决方案:用 useState/useReducer 存储响应式状态。
总结
Hooks 的核心是「让函数组件拥有状态和副作用能力」,面试中重点考察:
-
基础 Hooks 的原理和使用场景(
useState/useEffect/useContext); -
进阶 Hooks 的缓存逻辑(
useCallback/useMemo)、执行时机(useLayoutEffect); -
React 18 新增 Hooks 的优先级调度(
useTransition)、跨端兼容(useId); -
自定义 Hooks 的设计思路和性能优化;
-
闭包陷阱、批量更新、并发渲染等底层逻辑。