React Hooks 全解析

83 阅读14分钟

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 的「副作用队列」,执行时机分三类:

      1. 依赖项为空 []:仅组件挂载后执行,卸载前执行清理函数;

      2. 依赖项有值 [a, b]:挂载后 + 依赖项变化后执行;

      3. 无依赖项:每次渲染后执行;

    • 默认在浏览器绘制完成后异步执行(不阻塞渲染),对比 useLayoutEffect(绘制前同步执行)。

  • 执行时机:

    1. 首次渲染后执行(对应 componentDidMount);

    2. 依赖项变化后执行(对应 componentDidUpdate);

    3. 组件卸载前执行清理函数(对应 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 包裹组件树并传递 valueuseContext 读取当前上下文的 value;

  • 核心特性:Providervalue 变化时,所有使用 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 导致的不必要重渲染?

    1. 拆分细粒度 Context(如主题 Context、用户 Context 分开);

    2. 配合 React.memo 包装子组件;

    3. 将不变的状态抽离(如方法绑定到父组件,避免 value 频繁变化)。

  • Q3:Context 默认值什么时候生效?

✅ 回答:组件没有匹配的 Provider 时生效,而非 Provider 的 value 为 undefined 时。

  • Q4:useContext 和 Redux 的区别?

✅ 回答:

维度useContextRedux
定位跨组件状态共享全局状态管理
中间件支持支持(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 的区别?

✅ 回答:

维度useStateuseReducer
适用场景简单独立状态复杂关联状态
逻辑组织状态更新逻辑散在组件中集中在 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?

    1. 函数不传递给子组件;

    2. 子组件未用 React.memo;

    3. 依赖项频繁变化(缓存开销 > 重渲染开销)。

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 的核心区别?

✅ 回答:

维度useLayoutEffectuseEffect
执行时机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 时(如 useRequestusePagination),增强调试体验。

面试常问点
  • 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 —— 同步外部数据源

定义&原理

同步外部数据源(如 ReduxlocalStorage、全局变量)到 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 生成 → useInsertionEffectuseLayoutEffect → 浏览器绘制。
语法
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 是首次渲染的闭包值,无法获取最新值;

  • 解决方案:

    1. useRef 存储最新状态,在闭包中访问 ref.current

    2. 依赖项中加入该 state(触发 useEffect 重新执行);

    3. 使用 useReducer,通过 dispatch 获取最新状态。

3. Hooks 对比类组件的优势?

  • 逻辑复用更简单(自定义 Hooks 替代高阶组件/Render Props);

  • 代码更简洁(函数式写法,无 this 绑定、生命周期耦合);

  • 支持并发渲染(React 18+ Hooks 适配);

  • 无 this 指向混乱问题。

4. 如何优化 Hooks 组件性能?

  • 减少不必要重渲染:

    1. React.memo 包装组件;

    2. useCallback 缓存函数,useMemo 缓存计算结果;

  • 优化副作用:

    1. 正确设置 useEffect 依赖项;

    2. 清理副作用(定时器、订阅);

  • 并发优化: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 的设计思路和性能优化;

  • 闭包陷阱、批量更新、并发渲染等底层逻辑。