React 高频面试题

88 阅读31分钟

React 高频面试题

1. React 的核心概念是什么?

React 是一个用于构建用户界面的 JavaScript 库,核心思想:UI = f(state)

核心概念说明
JSXJavaScript 语法扩展,编译为 React.createElement() 调用
组件UI 的基本构建单元,分函数组件和类组件
单向数据流数据从父组件通过 props 向下流动
虚拟 DOM内存中的轻量 DOM 表示,通过 diff 算法最小化真实 DOM 操作
声明式描述 UI 应该是什么样子,而不是如何操作 DOM
// ========== 函数组件(推荐) ==========
const Greeting = ({ name, age = 18 }) => {
  return (
    <div className="greeting">
      <h1>Hello, {name}!</h1>
      <p>Age: {age}</p>
    </div>
  )
}

// ========== JSX 本质 ==========
// JSX 是 React.createElement 的语法糖
// <div className="app">Hello</div>
// 编译为:
React.createElement('div', { className: 'app' }, 'Hello')

// JSX 中的条件渲染
const App = ({ isLoggedIn, items }) => (
  <div>
    {/* 三元表达式 */}
    {isLoggedIn ? <Dashboard /> : <Login />}

    {/* 短路求值 */}
    {isLoggedIn && <Welcome />}

    {/* 列表渲染(必须有 key) */}
    {items.map(item => <Item key={item.id} data={item} />)}
  </div>
)

// ========== 函数组件 vs 类组件 ==========
// ✅ 函数组件(现代 React 推荐)
function FuncComp({ count }) {
  // 每次渲染都捕获当前的 props/state(闭包特性)
  const handleClick = () => {
    setTimeout(() => alert(count), 3000)  // 总是弹出点击时的 count
  }
  return <button onClick={handleClick}>{count}</button>
}

// ❌ 类组件(this.props 是可变的)
class ClassComp extends React.Component {
  handleClick = () => {
    setTimeout(() => alert(this.props.count), 3000)  // 弹出最新的 count(可能已变)
  }
  render() {
    return <button onClick={this.handleClick}>{this.props.count}</button>
  }
}

💡 面试加分点: 函数组件和类组件的本质区别不仅是语法——函数组件每次渲染都捕获当前的 props 和 state(闭包),而类组件通过 this 读取的总是最新值。这就是著名的 "Capture Value" 特性。React 官方已明确推荐函数组件 + Hooks。


2. React 的 Context 是什么?

Context 提供了一种在组件树中共享数据的方式,无需通过 props 逐层传递。

对比项Props DrillingContext状态管理库
层级限制逐层传递跨层级跨层级
性能✅ 按需渲染⚠️ Provider value 变化时所有消费者重渲染✅ 精确更新
适用场景1-2 层主题/语言/认证等低频变化高频更新的全局状态
import { createContext, useContext, useState, useMemo } from 'react'

// ========== 创建 Context ==========
const ThemeContext = createContext({ theme: 'light', toggleTheme: () => {} })

// ========== Provider 组件 ==========
const ThemeProvider = ({ children }) => {
  const [theme, setTheme] = useState('light')
  const toggleTheme = () => setTheme(t => t === 'light' ? 'dark' : 'light')

  // ⚠️ 必须用 useMemo 缓存 value,否则 Provider 每次渲染都创建新对象
  // 导致所有消费者都重渲染
  const value = useMemo(() => ({ theme, toggleTheme }), [theme])

  return (
    <ThemeContext.Provider value={value}>
      {children}
    </ThemeContext.Provider>
  )
}

// ========== 自定义 Hook 封装(推荐模式) ==========
const useTheme = () => {
  const context = useContext(ThemeContext)
  if (!context) throw new Error('useTheme must be used within ThemeProvider')
  return context
}

// ========== 消费 Context ==========
const ThemedButton = () => {
  const { theme, toggleTheme } = useTheme()
  return (
    <button
      style={{ background: theme === 'dark' ? '#333' : '#fff', color: theme === 'dark' ? '#fff' : '#333' }}
      onClick={toggleTheme}
    >
      切换主题(当前:{theme})
    </button>
  )
}

// ========== 多 Context 组合 ==========
const AuthContext = createContext(null)
const I18nContext = createContext('zh')

const App = () => (
  <AuthProvider>
    <ThemeProvider>
      <I18nProvider>
        <Page />
      </I18nProvider>
    </ThemeProvider>
  </AuthProvider>
)

// ========== 性能优化:拆分 Context ==========
// ❌ 错误:一个大 Context 放所有状态
const AppContext = createContext({ theme: '', user: null, locale: '' })

// ✅ 正确:按职责拆分为多个小 Context
const ThemeCtx = createContext('light')
const UserCtx = createContext(null)
const LocaleCtx = createContext('zh')
// 修改 theme 不会导致只用 user 的组件重渲染

💡 面试加分点:

Context 最大的性能问题是 Provider value 变化时,所有消费者都会重渲染,即使它们只用了 value 中的部分属性。

解决方案:

1️⃣ useMemo 缓存 value

2️⃣ 拆分为多个小 Context

3️⃣ 使用 React.memo 阻断传播

4️⃣ 如果状态更新频繁,考虑用 Zustand/Jotai 替代。


3. React 中 key 的作用和 Reconciliation 原理

key 的核心作用

key 是 React 在 Reconciliation(协调/调和)过程中用来标识元素身份的唯一属性,决定了元素是"复用更新"还是"销毁重建"。

场景有唯一 key无 key(或用 index)
列表增删✅ 精确匹配,最小化 DOM 操作❌ 按位置复用,可能状态错乱
列表排序✅ 移动 DOM 节点❌ 逐个更新内容,效率低
条件切换✅ key 不同 → 重建组件key 相同 → 复用(可能有 Bug)

为什么不能用 index 做 key?

// ========== index 做 key 的经典 Bug ==========
function TodoList() {
  const [todos, setTodos] = useState([
    { id: 1, text: 'Learn React' },
    { id: 2, text: 'Learn Vue' },
    { id: 3, text: 'Learn Angular' }
  ])

  const removeFirst = () => {
    setTodos(todos.slice(1))  // 删除第一项
  }

  return (
    <div>
      {/* ❌ 用 index 做 key */}
      {todos.map((todo, index) => (
        <div key={index}>
          <span>{todo.text}</span>
          <input defaultValue={todo.text} />
          {/* 删除 "Learn React" 后:
              index=0 对应 "Learn Vue",但 input 还是 "Learn React" 的内容!
              因为 React 认为 key=0 的元素还在,只是更新了 text prop
              而 input 是非受控的,defaultValue 不会再更新 → 状态错乱! */}
        </div>
      ))}

      {/* ✅ 用唯一 id 做 key */}
      {todos.map(todo => (
        <div key={todo.id}>
          <span>{todo.text}</span>
          <input defaultValue={todo.text} />
          {/* 删除 "Learn React" 后:key=1 的 DOM 被移除,key=2 和 key=3 复用 → 正确! */}
        </div>
      ))}

      <button onClick={removeFirst}>删除第一项</button>
    </div>
  )
}

key 在 Reconciliation 中的工作原理

// ========== React Diff 中 key 的匹配逻辑 ==========

// React 的 Reconciliation 规则:
// 1. 不同类型的元素 → 销毁旧树,创建新树
// 2. 相同类型的 DOM 元素 → 只更新变化的属性
// 3. 相同类型的组件 → 保留实例,更新 props
// 4. 列表中的元素 → 用 key 匹配新旧节点

// 有 key 的列表 diff 过程(简化):
// 旧: [A:key1] [B:key2] [C:key3]
// 新: [C:key3] [A:key1] [B:key2]
//
// 第一轮遍历:逐位比较(A vs C → key 不同,跳出)
// 构建旧节点 Map:{ key1: A, key2: B, key3: C }
// 遍历新列表:
//   C:key3 → Map 中找到,复用并移动
//   A:key1 → Map 中找到,复用并移动
//   B:key2 → Map 中找到,复用并移动
// 结果:只是移动了 DOM 节点,没有销毁重建

// ========== key 的高级用法:强制重建组件 ==========
function App() {
  const [userId, setUserId] = useState(1)

  return (
    <div>
      {/* ✅ key 变化 → UserProfile 整个重建(重置所有状态) */}
      <UserProfile key={userId} userId={userId} />

      {/* 不用 key 或 key 不变 → 组件复用,只更新 props */}
      {/* 如果组件内部有 useState 等状态,状态不会重置 */}

      <button onClick={() => setUserId(2)}>切换用户</button>
    </div>
  )
}

// ========== key 重建 vs useEffect 重置 ==========
// 方式1:key 变化强制重建(简单粗暴,推荐)
<Form key={formId} initialData={data} />

// 方式2:useEffect 监听 prop 变化重置状态(需要手动维护)
function Form({ initialData }) {
  const [formData, setFormData] = useState(initialData)
  useEffect(() => {
    setFormData(initialData)  // 需要手动重置每个状态
  }, [initialData])
}

key 的本质:JSX 元素的"身份证"

// ========== key 是 React Element 的特殊属性 ==========
// JSX 编译后:
<li key="1">A</li>
// ↓ 编译为
React.createElement('li', { key: '1' }, 'A')
// ↓ 生成 ReactElement
{ type: 'li', key: '1', props: { children: 'A' }, ref: null, $$typeof: Symbol(react.element) }

// 注意:key 不是普通 props,组件内部无法通过 props.key 访问
function Item(props) {
  console.log(props.key)  // ❌ undefined(key 被 React 拦截)
  console.log(props.id)   // ✅ 正常访问
  return <li>{props.id}</li>
}
// 如果需要在组件内拿到 key,必须额外传:<Item key={id} id={id} />

key 的取值规则与最佳实践

取值方式是否推荐说明
业务唯一 ID(数据库主键)✅ 强烈推荐最稳定,数据生命周期内不变
UUID(创建时生成并保存)✅ 推荐适合前端纯本地数据(如待办事项)
多字段拼接(如 ${type}-${id})✅ 推荐跨数据源合并列表时使用
数组 index⚠️ 仅在静态列表使用列表只渲染不增删不排序才安全
Math.random() / Date.now()❌ 禁止每次渲染都变 → 完全失去 diff 优化,列表整体重建
对象引用(如 key={item})❌ 禁止React 会调用 String(key) → [object Object],全部冲突
不传 key⚠️ 默认用 indexReact 会警告(开发环境),等价于 index
// ========== 不同场景下的 key 选择 ==========

// 场景1:后端返回的数据 → 用业务 id
{users.map(u => <UserItem key={u.id} user={u} />)}

// 场景2:纯前端创建的临时数据 → 创建时生成稳定 id
const addTodo = (text) => {
  setTodos([...todos, { id: crypto.randomUUID(), text }])  // ✅ 创建时生成
  // ❌ 不要在 render 里 todos.map((t, i) => ({ ...t, id: crypto.randomUUID() }))
}

// 场景3:合并多个数据源 → 加前缀
{[
  ...localPosts.map(p => ({ ...p, _key: `local-${p.id}` })),
  ...remotePosts.map(p => ({ ...p, _key: `remote-${p.id}` }))
].map(p => <Post key={p._key} {...p} />)}

// 场景4:嵌套数组 / Fragment
{groups.map(group => (
  <React.Fragment key={group.id}>  {/* Fragment 也支持 key */}
    <h3>{group.name}</h3>
    {group.items.map(item => (
      <Item key={item.id} data={item} />  {/* 内层独立 key */}
    ))}
  </React.Fragment>
))}
// 注意:<>...</> 短语法不支持 key,必须用 React.Fragment

key 的作用域:兄弟节点之间唯一即可

// ========== key 只在同一父节点的兄弟节点间比较 ==========
<div>
  <ul>
    <li key="1">A</li>  {/* 这个 1 和下面的 1 不冲突 */}
    <li key="2">B</li>
  </ul>
  <ul>
    <li key="1">X</li>  {/* 不同的父节点,key 可以重复 */}
    <li key="2">Y</li>
  </ul>
</div>

// ❌ 但同一父节点下重复 key 会警告并导致 diff 错乱
<ul>
  <li key="1">A</li>
  <li key="1">B</li>  {/* Warning: Encountered two children with the same key */}
</ul>

key 强制重建组件的进阶场景

// ========== 场景1:路由切换重置表单 ==========
function EditPage() {
  const { userId } = useParams()
  // 切换 userId 时,整个 UserForm 重建,所有 useState/useRef 都重置
  return <UserForm key={userId} userId={userId} />
}

// ========== 场景2:错误边界重置 ==========
function App() {
  const [errorKey, setErrorKey] = useState(0)
  return (
    <ErrorBoundary key={errorKey} onReset={() => setErrorKey(k => k + 1)}>
      <Dashboard />
    </ErrorBoundary>
  )
}

// ========== 场景3:动画重新触发 ==========
function Toast({ message }) {
  // 每次 message 变化,整个组件重建,CSS 动画重新执行
  return <div key={message} className="toast-fade-in">{message}</div>
}

// ========== 场景4:受控 → 非受控切换重置 ==========
// React 官方推荐:用 key 重置非受控组件状态
<input key={resetCount} defaultValue="" />
// resetCount++ → input 重建 → 清空内容

key 相关的常见坑

// ========== 坑1:用 index 做 key + 有状态子组件 ==========
{list.map((item, i) => <ItemWithState key={i} data={item} />)}
// 删除中间一项时:后面所有 ItemWithState 内部状态都会"前移"错位

// ========== 坑2:动态生成的 key 每次都不同 ==========
function Bad() {
  return list.map(item => <Item key={`${item.id}-${Date.now()}`} />)
  // 每次渲染 Date.now() 都变 → 整个列表全部销毁重建
}

// ========== 坑3:key 类型不一致 ==========
<li key={1}>A</li>     // key 实际是字符串 "1"
<li key="1">B</li>     // 也是 "1"
// React 内部会 toString,数字 1 和字符串 "1" 视为相同 key

// ========== 坑4:动画/过渡场景错把 key 当业务标识 ==========
// react-transition-group / framer-motion 的 AnimatePresence
// 必须保证 key 在元素生命周期内稳定,否则进退场动画异常
<AnimatePresence>
  {visible && <motion.div key="modal" exit={{ opacity: 0 }}>...</motion.div>}
</AnimatePresence>

// ========== 坑5:跨父节点移动 ==========
// React 的 diff 不会跨父节点复用 DOM
// 把一个有 key 的节点从 <ul>A 移到 <ul>B → 仍然是销毁+重建
// 想跨容器复用 → 用 createPortal 或保持父节点不变

key 的源码视角(Fiber Reconciliation)

// ========== React 源码:reconcileChildrenArray 简化逻辑 ==========
// 文件:react-reconciler/src/ReactChildFiber.js

function reconcileChildrenArray(returnFiber, currentFirstChild, newChildren) {
  // 阶段1:从头开始按顺序匹配(fast path)
  let oldFiber = currentFirstChild
  let newIdx = 0
  for (; oldFiber !== null && newIdx < newChildren.length; newIdx++) {
    // key 相同且 type 相同 → 复用 Fiber
    if (oldFiber.key === newChildren[newIdx].key) {
      // 更新 props,标记 Placement / Update
    } else {
      break  // key 不匹配 → 跳出,进入阶段3
    }
    oldFiber = oldFiber.sibling
  }

  // 阶段2:新列表遍历完,删除剩余旧 Fiber
  if (newIdx === newChildren.length) {
    deleteRemainingChildren(returnFiber, oldFiber)
    return
  }

  // 阶段3:旧 Fiber 放进 Map(key → Fiber),新列表用 key 查找
  const existingChildren = mapRemainingChildren(returnFiber, oldFiber)
  for (; newIdx < newChildren.length; newIdx++) {
    const matchedFiber = existingChildren.get(
      newChildren[newIdx].key ?? newIdx  // 没 key 用 index 兜底
    )
    if (matchedFiber) {
      // 复用:从 Map 删除,标记移动
      existingChildren.delete(newChildren[newIdx].key ?? newIdx)
    } else {
      // 没匹配到:创建新 Fiber
    }
  }

  // 阶段4:Map 里剩下的旧 Fiber → 全部删除
  existingChildren.forEach(child => deleteChild(returnFiber, child))
}

核心结论:React 的 diff 是 O(n) 算法,前提是有稳定的 key。没有 key 时退化为按位置比较,列表头部插入会导致全列表重渲染。

面试速答模板

key 是什么:React 给同层兄弟节点的"身份证",是 Reconciliation(协调)阶段匹配新旧 Fiber 的依据。

作用:让 React 在 O(n) 时间内识别哪些节点是复用、移动、新增、删除,决定 DOM 操作和组件状态去留。

三大规则:

  1. 稳定——同一数据生命周期内 key 不变(用业务 id,禁用 Math.random)
  2. 唯一——同层兄弟节点间唯一(不需要全局唯一)
  3. 可预测——不要用 index 做有状态列表的 key(增删会导致状态错乱)

进阶用法:用 key 变化强制重建组件,是重置组件状态最优雅的方式(替代 useEffect 监听 prop 重置 state)。

💡 面试加分点: key 的本质是给 Reconciliation 提供"节点身份标识"。三个核心规则:1)列表中用唯一业务 id 做 key,不要用 index(有状态子元素会错乱);2)用 key 强制重建组件是重置组件状态的最简单方法(比 useEffect 监听重置更优雅);3)key 必须在兄弟节点中唯一(不需要全局唯一)。React 的 Fiber 架构中,key 是 createFiberFromElement 时用于匹配旧 Fiber 的关键依据,源码中通过 existingChildren Map 实现 O(1) 查找,没 key 时退化为 index 匹配,列表头部插入会触发全列表重渲染。


4. React 的 Diff 算法和 Fiber 架构?

Diff 算法(Reconciliation)

React 的 diff 算法基于三个策略将 O(n³) 降为 O(n):

策略说明
Tree Diff只比较同层级节点,不跨层移动
Component Diff不同类型的组件直接替换整棵子树
Element Diff同层子元素通过 key 标识移动、新增、删除
// ========== key 的重要性 ==========
// ❌ 用 index 作为 key(列表增删时 DOM 复用错误)
items.map((item, index) => <li key={index}>{item.name}</li>)
// 在头部插入新元素时:所有 li 都会重新渲染(key 全部错位)

// ✅ 用唯一 ID 作为 key
items.map(item => <li key={item.id}>{item.name}</li>)
// 只渲染新插入的 li,其他 li 直接复用

// ❌ 用随机数作为 key
items.map(item => <li key={Math.random()}>{item.name}</li>)
// 每次渲染都是新 key,完全失去 diff 优化

Fiber 架构(React 16+)

对比项Stack Reconciler(旧)Fiber Reconciler(新)
执行方式递归,同步不可中断链表,异步可中断
更新优先级无有(紧急/非紧急)
帧率影响长任务阻塞页面时间切片,保证 60fps
并发支持❌✅
Fiber 工作流程(双缓冲树):
┌─────────────┐     ┌─────────────┐
│  current 树  │ ←→ │ workInProgress│
│  (当前屏幕)  │     │  (构建中)    │
└─────────────┘     └─────────────┘
        ↑                    │
        └── commit 阶段交换 ──┘

Fiber 节点结构(链表):
FiberNode {
  type,           // 组件类型
  key,
  stateNode,      // 真实 DOM / 类组件实例
  child,          // 第一个子节点
  sibling,        // 下一个兄弟节点
  return,         // 父节点
  memoizedState,  // 当前 state(Hook 链表)
  flags,          // 副作用标记(Placement/Update/Deletion)
}

两个阶段:
┌─────────────────────────────┐
│ Render 阶段(可中断)         │
│ - 构建 workInProgress 树     │
│ - 标记需要更新的节点(flags) │
│ - 可被高优先级任务打断       │
└─────────────────────────────┘
              ↓
┌─────────────────────────────┐
│ Commit 阶段(不可中断)       │
│ - 操作真实 DOM               │
│ - 执行副作用(useEffect 等) │
│ - 一次性同步完成             │
└─────────────────────────────┘

💡 面试加分点: Fiber 的核心是将递归变为链表遍历,每个 Fiber 节点是一个工作单元,可以暂停/恢复。React 18 的 useTransition、useDeferredValue 依赖 Fiber 的优先级调度。Render 阶段可中断,Commit 阶段不可中断——因为操作真实 DOM 必须一次性完成。


5. React 的性能优化方法?

分类优化手段说明
渲染层React.memo浅比较 props,跳过不必要渲染
useMemo / useCallback缓存值和函数引用
状态下沉将频繁变化的状态移到更小的子组件
避免 Context 滥用拆分 Context,减少不必要的消费者更新
加载层代码分割React.lazy() + Suspense
路由懒加载按路由动态 import
图片懒加载loading="lazy" / IntersectionObserver
列表层虚拟列表react-window / react-virtuoso
稳定 key不用 index 做 key
网络层数据缓存TanStack Query / SWR
预加载<link rel="preload"> / prefetch
// ========== 1. React.memo ==========
const Child = React.memo(({ name, onClick }) => {
  console.log('Child 渲染')
  return <button onClick={onClick}>{name}</button>
})

// ========== 2. 代码分割 + 懒加载 ==========
import { lazy, Suspense } from 'react'
const HeavyPage = lazy(() => import('./HeavyPage'))
// 带 loading 的懒加载
const App = () => (
  <Suspense fallback={<div>Loading...</div>}>
    <HeavyPage />
  </Suspense>
)

// ========== 3. 虚拟列表(react-window) ==========
import { FixedSizeList } from 'react-window'
const VirtualList = ({ items }) => (
  <FixedSizeList height={400} itemCount={items.length} itemSize={50} width="100%">
    {({ index, style }) => (
      <div style={style}>{items[index].name}</div>
    )}
  </FixedSizeList>
)

// ========== 4. 状态下沉(提升性能最有效的方式之一) ==========
// ❌ 整个 App 因为 input 变化而重渲染
const App = () => {
  const [text, setText] = useState('')
  return (
    <div>
      <input value={text} onChange={e => setText(e.target.value)} />
      <ExpensiveTree />  {/* 每次输入都重渲染 */}
    </div>
  )
}

// ✅ 将频繁变化的状态下沉到独立组件
const SearchInput = () => {
  const [text, setText] = useState('')
  return <input value={text} onChange={e => setText(e.target.value)} />
}
const App = () => (
  <div>
    <SearchInput />
    <ExpensiveTree />  {/* 不再重渲染 */}
  </div>
)

// ========== 5. 避免在 render 中创建新对象/函数 ==========
// ❌ 每次渲染都创建新对象/函数
<Child style={{ color: 'red' }} onClick={() => doSomething()} />

// ✅ 提取到外部或用 useMemo/useCallback
const style = useMemo(() => ({ color: 'red' }), [])
const handleClick = useCallback(() => doSomething(), [])
<Child style={style} onClick={handleClick} />

💡 面试加分点: 性能优化的优先级:状态下沉 > 组件拆分 > React.memo > useMemo/useCallback。很多时候不需要 memo,只需要把频繁变化的状态移到更小的子组件就能解决问题。React DevTools 的 Profiler 可以帮助识别不必要的重渲染。


6. 自定义 Hook 是什么?如何编写?

自定义 Hook 是以 use 开头的函数,用于封装可复用的有状态逻辑。

Hook 规则说明
必须以 use 开头React 通过命名约定识别 Hook
只能在函数组件或其他 Hook 中调用不能在普通函数/循环/条件中调用
调用顺序必须一致不能放在 if/for 里(Hook 依赖调用顺序形成链表)
import { useState, useEffect, useCallback, useSyncExternalStore } from 'react'

// ========== 1. useFetch:封装数据请求 ==========
function useFetch(url) {
  const [data, setData] = useState(null)
  const [loading, setLoading] = useState(true)
  const [error, setError] = useState(null)

  const refetch = useCallback(() => {
    let cancelled = false
    setLoading(true)
    setError(null)

    fetch(url)
      .then(res => { if (!res.ok) throw new Error(res.statusText); return res.json() })
      .then(data => { if (!cancelled) setData(data) })
      .catch(err => { if (!cancelled) setError(err) })
      .finally(() => { if (!cancelled) setLoading(false) })

    return () => { cancelled = true }
  }, [url])

  useEffect(() => {
    const cleanup = refetch()
    return cleanup
  }, [refetch])

  return { data, loading, error, refetch }
}

// ========== 2. useLocalStorage:本地存储同步 ==========
function useLocalStorage(key, initialValue) {
  const [value, setValue] = useState(() => {
    try {
      const item = localStorage.getItem(key)
      return item ? JSON.parse(item) : initialValue
    } catch {
      return initialValue
    }
  })

  const setStoredValue = useCallback((newValue) => {
    const val = typeof newValue === 'function' ? newValue(value) : newValue
    setValue(val)
    localStorage.setItem(key, JSON.stringify(val))
  }, [key, value])

  return [value, setStoredValue]
}

// ========== 3. useDebounce:防抖 ==========
function useDebounce(value, delay = 300) {
  const [debouncedValue, setDebouncedValue] = useState(value)
  useEffect(() => {
    const timer = setTimeout(() => setDebouncedValue(value), delay)
    return () => clearTimeout(timer)
  }, [value, delay])
  return debouncedValue
}

// ========== 4. useToggle:开关状态 ==========
function useToggle(initial = false) {
  const [value, setValue] = useState(initial)
  const toggle = useCallback(() => setValue(v => !v), [])
  const setTrue = useCallback(() => setValue(true), [])
  const setFalse = useCallback(() => setValue(false), [])
  return [value, { toggle, setTrue, setFalse }]
}

// ========== 5. usePrevious:获取上一次的值 ==========
function usePrevious(value) {
  const ref = useRef()
  useEffect(() => { ref.current = value })
  return ref.current
}

// ========== 6. useMediaQuery:响应式断点 ==========
function useMediaQuery(query) {
  const subscribe = useCallback(
    (callback) => {
      const mql = window.matchMedia(query)
      mql.addEventListener('change', callback)
      return () => mql.removeEventListener('change', callback)
    },
    [query]
  )
  const getSnapshot = () => window.matchMedia(query).matches
  return useSyncExternalStore(subscribe, getSnapshot)
}

// ========== 使用示例 ==========
const SearchPage = () => {
  const [query, setQuery] = useState('')
  const debouncedQuery = useDebounce(query, 300)
  const { data, loading, error, refetch } = useFetch(`/api/search?q=${debouncedQuery}`)
  const [theme, setTheme] = useLocalStorage('theme', 'light')
  const [isOpen, { toggle }] = useToggle(false)
  const isMobile = useMediaQuery('(max-width: 768px)')

  return (
    <div>
      <input value={query} onChange={e => setQuery(e.target.value)} />
      {loading ? <div>Loading...</div> : <ResultList data={data} />}
      {error && <button onClick={refetch}>重试</button>}
    </div>
  )
}

💡 面试加分点: 自定义 Hook 的本质是逻辑复用——将有状态的逻辑从组件中提取出来,多个组件共享同一套逻辑但各自拥有独立的状态。Hook 规则中「调用顺序必须一致」的原因是 React 内部用链表存储 Hook 状态,靠调用顺序来匹配对应的状态节点。


7. React 18 的新特性?

特性说明
createRoot新的根 API,启用并发特性
自动批处理所有场景下的状态更新都自动批处理
useTransition标记非紧急更新,可被中断
useDeferredValue延迟更新的值,保持 UI 响应
useId生成唯一 ID(SSR 安全)
Suspense 增强支持 SSR 流式渲染
严格模式增强开发环境双重调用 Effect
// ========== 1. 新根 API ==========
import { createRoot } from 'react-dom/client'
const root = createRoot(document.getElementById('root'))
root.render(<App />)
// 旧 API(React 17):ReactDOM.render(<App />, container)

// ========== 2. 自动批处理(Automatic Batching) ==========
// React 17:只有事件处理函数中的更新会批处理
// React 18:所有场景都自动批处理(setTimeout、Promise、原生事件等)
setTimeout(() => {
  setCount(c => c + 1)
  setFlag(f => !f)
  // React 18:只触发一次重渲染 ✅
  // React 17:触发两次重渲染 ❌
}, 0)

// 需要强制不批处理时(极少使用)
import { flushSync } from 'react-dom'
flushSync(() => setCount(c => c + 1))  // 立即更新
flushSync(() => setFlag(f => !f))       // 立即更新

// ========== 3. useTransition:非紧急更新 ==========
import { useState, useTransition } from 'react'
const SearchPage = () => {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])
  const [isPending, startTransition] = useTransition()

  const handleChange = (e) => {
    setQuery(e.target.value)  // 紧急更新:立即响应输入
    startTransition(() => {
      setResults(searchData(e.target.value))  // 非紧急:可被中断
    })
  }

  return (
    <div>
      <input value={query} onChange={handleChange} />
      {isPending ? <div>搜索中...</div> : <ResultList results={results} />}
    </div>
  )
}

// ========== 4. useDeferredValue:延迟更新 ==========
import { useDeferredValue, useMemo } from 'react'
const SearchResults = ({ query }) => {
  const deferredQuery = useDeferredValue(query)
  const isStale = query !== deferredQuery  // 是否正在过渡
  const results = useMemo(() => searchData(deferredQuery), [deferredQuery])
  return (
    <div style={{ opacity: isStale ? 0.5 : 1 }}>
      <ResultList results={results} />
    </div>
  )
}

// ========== 5. useId:生成唯一 ID ==========
import { useId } from 'react'
const FormField = ({ label }) => {
  const id = useId()  // 生成稳定的唯一 ID(如 ":r1:")
  return (
    <div>
      <label htmlFor={id}>{label}</label>
      <input id={id} />
    </div>
  )
}
// 好处:在 SSR 中客户端和服务端生成相同的 ID,避免 hydration 不匹配

💡 面试加分点: useTransition 和 useDeferredValue 的区别:useTransition 包裹 状态更新的代码,而 useDeferredValue 包裹 值本身。当你能控制状态更新用 useTransition,当值从 props 传入用 useDeferredValue。自动批处理是 React 18 最重要的默认行为变化。


8. React 中的错误边界(Error Boundary)?

错误边界是一种 React 组件,能够捕获子组件树中的 JavaScript 错误,显示降级 UI。

能捕获不能捕获
子组件渲染期间的错误事件处理函数中的错误
生命周期方法中的错误异步代码(setTimeout、Promise)
构造函数中的错误服务端渲染的错误
错误边界自身的错误
import { Component } from 'react'

// ========== 错误边界(只能用类组件实现) ==========
class ErrorBoundary extends Component {
  constructor(props) {
    super(props)
    this.state = { hasError: false, error: null, errorInfo: null }
  }

  // 渲染阶段调用:用于返回降级 state
  static getDerivedStateFromError(error) {
    return { hasError: true, error }
  }

  // commit 阶段调用:用于记录错误信息
  componentDidCatch(error, errorInfo) {
    console.error('Error caught:', error, errorInfo)
    // 上报到 Sentry 等监控系统
    reportError({ error, errorInfo, componentStack: errorInfo.componentStack })
  }

  handleReset = () => {
    this.setState({ hasError: false, error: null, errorInfo: null })
  }

  render() {
    if (this.state.hasError) {
      // 自定义 fallback 或默认 UI
      return this.props.fallback?.(this.state.error, this.handleReset) || (
        <div>
          <h2>出错了</h2>
          <p>{this.state.error?.message}</p>
          <button onClick={this.handleReset}>重试</button>
        </div>
      )
    }
    return this.props.children
  }
}

// ========== 使用方式 ==========
const App = () => (
  <ErrorBoundary fallback={(error, reset) => (
    <div>
      <p>页面加载失败:{error.message}</p>
      <button onClick={reset}>重试</button>
    </div>
  )}>
    <MyComponent />
  </ErrorBoundary>
)

// ========== 推荐:使用 react-error-boundary 库 ==========
import { ErrorBoundary } from 'react-error-boundary'
const App = () => (
  <ErrorBoundary
    fallbackRender={({ error, resetErrorBoundary }) => (
      <div>
        <p>Error: {error.message}</p>
        <button onClick={resetErrorBoundary}>重试</button>
      </div>
    )}
    onReset={() => queryClient.invalidateQueries()}  // 重试时清除缓存
    onError={(error, info) => logErrorToService(error, info)}
  >
    <Routes />
  </ErrorBoundary>
)

// ========== 事件处理中的错误需要自行 try-catch ==========
const Button = () => {
  const handleClick = () => {
    try {
      doSomething()
    } catch (error) {
      // 错误边界捕获不到这里的错误
      reportError(error)
    }
  }
  return <button onClick={handleClick}>Click</button>
}

💡 面试加分点: 错误边界不能用函数组件实现——因为 getDerivedStateFromError 和 componentDidCatch 没有对应的 Hook。生产中推荐使用 react-error-boundary 库,它提供了 useErrorBoundary Hook 让函数组件也能主动触发错误边界。


9. React 中的 Portals 是什么?

Portal 将子组件渲染到 DOM 层级之外的位置,但 React 事件冒泡仍沿组件树传播。

import { createPortal } from 'react-dom'

// ========== 典型用途:弹窗/Modal ==========
const Modal = ({ isOpen, onClose, children }) => {
  if (!isOpen) return null

  return createPortal(
    <div className="modal-overlay" onClick={onClose}>
      <div className="modal-content" onClick={e => e.stopPropagation()}>
        {children}
        <button onClick={onClose}>关闭</button>
      </div>
    </div>,
    document.body  // 渲染到 body 下,避免 z-index 和 overflow 问题
  )
}

// ========== 事件冒泡行为 ==========
// 即使 Portal 渲染到了 document.body,事件仍然沿 React 组件树冒泡
const App = () => {
  // ✅ 这里能捕获到 Portal 内部按钮的点击事件!
  const handleClick = () => console.log('App 捕获到点击')

  return (
    <div onClick={handleClick}>
      <Modal isOpen={true} onClose={() => {}}>
        <button>点我</button>  {/* 事件会冒泡到 App 的 onClick */}
      </Modal>
    </div>
  )
}

💡 面试加分点: Portal 的关键特性:DOM 层级和 React 组件层级是分离的——DOM 上渲染到 body,但 React 事件仍沿组件树冒泡。常用于 Modal、Tooltip、Toast 等需要脱离父容器 overflow: hidden 或 z-index 限制的场景。


10. React 中的 forwardRef 和 useImperativeHandle?

forwardRef 让父组件能获取子组件的 DOM 引用,useImperativeHandle 控制暴露的方法。

import { forwardRef, useImperativeHandle, useRef, useState } from 'react'

// ========== forwardRef:转发 ref 到子组件 DOM ==========
const MyInput = forwardRef((props, ref) => {
  return (
    <div className="input-wrapper">
      <input ref={ref} {...props} />
    </div>
  )
})

// ========== useImperativeHandle:控制暴露的方法(而非整个 DOM) ==========
const FancyInput = forwardRef((props, ref) => {
  const inputRef = useRef(null)
  const [value, setValue] = useState('')

  // 只暴露特定的方法,而不是整个 DOM 节点
  useImperativeHandle(ref, () => ({
    focus: () => inputRef.current?.focus(),
    clear: () => { setValue(''); inputRef.current.value = '' },
    getValue: () => value,
    scrollIntoView: () => inputRef.current?.scrollIntoView({ behavior: 'smooth' }),
  }), [value])  // 依赖数组

  return <input ref={inputRef} value={value} onChange={e => setValue(e.target.value)} {...props} />
})

// 使用
const Form = () => {
  const inputRef = useRef(null)
  return (
    <div>
      <FancyInput ref={inputRef} placeholder="请输入..." />
      <button onClick={() => inputRef.current?.focus()}>聚焦</button>
      <button onClick={() => inputRef.current?.clear()}>清空</button>
      <button onClick={() => alert(inputRef.current?.getValue())}>获取值</button>
    </div>
  )
}

// ========== React 19:ref 作为普通 prop(不再需要 forwardRef) ==========
// React 19+
function MyInput({ ref, ...props }) {
  return <input ref={ref} {...props} />
}
// 直接使用,无需 forwardRef 包裹

💡 面试加分点: useImperativeHandle 遵循最小暴露原则——不要直接暴露整个 DOM 节点,只暴露必要的方法。React 19 将 ref 作为普通 prop 传递,forwardRef 将逐步废弃。


11. React 的组件复用模式对比?

模式说明优点缺点
自定义 Hook函数中封装状态逻辑✅ 最简洁、无嵌套只能复用逻辑,不能复用 UI
HOC接受组件返回新组件复用逻辑+UI 包装嵌套地狱、props 冲突、难调试
Render Props通过函数 prop 传递渲染逻辑灵活嵌套回调
组合组件children/Slot 模式✅ 直观适用场景有限
// ========== 1. HOC(高阶组件) ==========
const withAuth = (WrappedComponent, requiredRole) => {
  return function AuthComponent(props) {
    const { user } = useAuth()
    if (!user) return <Navigate to="/login" />
    if (requiredRole && !user.roles.includes(requiredRole)) return <div>没有权限</div>
    return <WrappedComponent {...props} user={user} />
  }
}

const withLoading = (WrappedComponent) => {
  return function LoadingComponent({ isLoading, ...props }) {
    if (isLoading) return <div className="spinner">Loading...</div>
    return <WrappedComponent {...props} />
  }
}

// ⚠️ HOC 嵌套地狱
const AdminPage = withAuth(withLoading(withLogger(AdminContent)), 'admin')

// ========== 2. Render Props ==========
const MouseTracker = ({ render }) => {
  const [pos, setPos] = useState({ x: 0, y: 0 })
  const handleMove = (e) => setPos({ x: e.clientX, y: e.clientY })
  return <div onMouseMove={handleMove}>{render(pos)}</div>
}
// <MouseTracker render={({ x, y }) => <p>鼠标位置:{x}, {y}</p>} />

// ========== 3. 自定义 Hook(推荐替代 HOC 和 Render Props) ==========
function useMousePosition() {
  const [pos, setPos] = useState({ x: 0, y: 0 })
  useEffect(() => {
    const handler = (e) => setPos({ x: e.clientX, y: e.clientY })
    window.addEventListener('mousemove', handler)
    return () => window.removeEventListener('mousemove', handler)
  }, [])
  return pos
}

// 使用:简洁明了,无嵌套
const Component = () => {
  const { x, y } = useMousePosition()
  return <p>鼠标位置:{x}, {y}</p>
}

💡 面试加分点: 现代 React 中 自定义 Hook 几乎完全取代了 HOC 和 Render Props——逻辑更清晰、没有嵌套地狱、没有 props 命名冲突。HOC 唯一还有用的场景是「需要在不修改组件的情况下包装它」(如路由守卫、权限控制等跨切面逻辑)。


12. React 中的状态管理方案对比

方案学习曲线包体积适用场景特点
useState + Context低0小型应用内置,但 Context 性能差
useReducer + Context低0中小型应用dispatch 稳定引用
Redux Toolkit中大大型团队项目生态完善、DevTools 强
Zustand✅ 低✅ 小 (2KB)中小型应用极简、hooks 原生
Jotai低小原子化状态类似 Recoil,更轻量
TanStack Query中中服务端状态缓存/重试/轮询
// ========== 1. useReducer + Context(小型应用) ==========
const AppContext = createContext(null)
const AppProvider = ({ children }) => {
  const [state, dispatch] = useReducer(reducer, initialState)
  const value = useMemo(() => ({ state, dispatch }), [state])
  return <AppContext.Provider value={value}>{children}</AppContext.Provider>
}

// ========== 2. Redux Toolkit(中大型应用) ==========
import { createSlice, configureStore } from '@reduxjs/toolkit'

const counterSlice = createSlice({
  name: 'counter',
  initialState: { value: 0 },
  reducers: {
    increment: (state) => { state.value++ },  // Immer:可以直接"修改"state
    decrement: (state) => { state.value-- },
    incrementByAmount: (state, action) => { state.value += action.payload },
  },
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

const store = configureStore({
  reducer: { counter: counterSlice.reducer }
})

// 组件中使用
import { useSelector, useDispatch } from 'react-redux'
const Counter = () => {
  const count = useSelector(state => state.counter.value)
  const dispatch = useDispatch()
  return (
    <div>
      <p>{count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
    </div>
  )
}

// ========== 3. Zustand(推荐,极简) ==========
import { create } from 'zustand'
import { devtools, persist } from 'zustand/middleware'

const useCounterStore = create(
  devtools(
    persist(
      (set, get) => ({
        count: 0,
        increment: () => set(state => ({ count: state.count + 1 })),
        decrement: () => set(state => ({ count: state.count - 1 })),
        reset: () => set({ count: 0 }),
        // 异步 action
        fetchCount: async () => {
          const res = await fetch('/api/count')
          const data = await res.json()
          set({ count: data.count })
        },
        // 通过 get() 访问当前状态
        doubleCount: () => get().count * 2,
      }),
      { name: 'counter-storage' }  // persist 到 localStorage
    )
  )
)

const Counter = () => {
  const count = useCounterStore(state => state.count)  // ✅ 选择性订阅,精确更新
  const increment = useCounterStore(state => state.increment)
  return <button onClick={increment}>{count}</button>
}

// ========== 4. Jotai(原子化状态) ==========
import { atom, useAtom } from 'jotai'

const countAtom = atom(0)
const doubleAtom = atom(get => get(countAtom) * 2)  // 派生原子

const Counter = () => {
  const [count, setCount] = useAtom(countAtom)
  const [double] = useAtom(doubleAtom)
  return <p>{count} × 2 = {double}</p>
}

💡 面试加分点: 状态管理选型原则:服务端状态用 TanStack Query/SWR(缓存、重试、乐观更新),客户端全局状态用 Zustand(简单、无 Provider、精确更新),大型团队用 Redux Toolkit(规范化、中间件生态、DevTools)。Context 适合低频变化的数据(主题、语言、认证信息)。


13. React Router v6 完整指南

路由配置方式

import { createBrowserRouter, RouterProvider, Outlet, Link, NavLink } from 'react-router-dom'

// ========== 方式一:对象式配置(推荐) ==========
const router = createBrowserRouter([
  {
    path: '/',
    element: <Layout />,
    errorElement: <ErrorPage />,  // 路由错误处理
    children: [
      { index: true, element: <Home /> },         // 默认子路由
      { path: 'about', element: <About /> },
      {
        path: 'users',
        element: <UserLayout />,
        children: [
          { index: true, element: <UserList /> },
          { path: ':userId', element: <UserDetail /> },  // 动态路由
          { path: ':userId/edit', element: <UserEdit /> },
        ]
      },
      {
        path: 'dashboard',
        lazy: () => import('./pages/Dashboard'),  // ✅ 路由懒加载(v6.4+)
      },
      { path: '*', element: <NotFound /> },  // 404
    ]
  }
])

const App = () => <RouterProvider router={router} />

// ========== 方式二:JSX 配置 ==========
import { BrowserRouter, Routes, Route } from 'react-router-dom'
const App = () => (
  <BrowserRouter>
    <Routes>
      <Route path="/" element={<Layout />}>
        <Route index element={<Home />} />
        <Route path="about" element={<About />} />
        <Route path="users/:userId" element={<UserDetail />} />
      </Route>
    </Routes>
  </BrowserRouter>
)

// ========== Layout 组件(使用 Outlet 渲染子路由) ==========
const Layout = () => (
  <div>
    <nav>
      <NavLink to="/" className={({ isActive }) => isActive ? 'active' : ''}>首页</NavLink>
      <NavLink to="/about">关于</NavLink>
      <NavLink to="/users">用户</NavLink>
    </nav>
    <main>
      <Outlet />  {/* 子路由渲染在这里 */}
    </main>
  </div>
)

路由 Hooks

import {
  useParams,         // 获取动态路由参数
  useSearchParams,   // 获取/设置查询参数
  useNavigate,       // 编程式导航
  useLocation,       // 当前 location 对象
  useMatch,          // 匹配指定路径
  useOutletContext,  // 获取父路由 Outlet 传递的 context
} from 'react-router-dom'

const UserDetail = () => {
  const { userId } = useParams()                      // /users/123 → { userId: '123' }
  const [searchParams, setSearchParams] = useSearchParams()  // ?tab=posts
  const navigate = useNavigate()
  const location = useLocation()                      // { pathname, search, hash, state }

  const tab = searchParams.get('tab') || 'profile'

  return (
    <div>
      <h1>用户 {userId}</h1>
      <button onClick={() => setSearchParams({ tab: 'posts' })}>文章</button>
      <button onClick={() => navigate(-1)}>返回</button>
      <button onClick={() => navigate('/users', { replace: true })}>回列表(replace)</button>
      <button onClick={() => navigate('/login', { state: { from: location } })}>
        登录(携带来源信息)
      </button>
    </div>
  )
}

路由三种模式

模式APIURL 示例特点
BrowserRoutercreateBrowserRouter/users/123HTML5 History,需服务器配置
HashRoutercreateHashRouter/#/users/123兼容旧浏览器,无需服务器配置
MemoryRoutercreateMemoryRouter无 URL 变化测试/非浏览器环境使用

💡 面试加分点: React Router v6 的重大变化:① <Switch> 改为 <Routes>;② component/render 改为 element(直接传 JSX);③ 移除了 exact(默认精确匹配);④ useHistory 改为 useNavigate;⑤ 新增 loader/action 数据路由 API(类似 Remix)。


14. React Router 路由守卫和权限控制

React Router 没有内置的路由守卫(不像 Vue Router),需要自己实现。

// ========== 1. 高阶组件式路由守卫 ==========
const ProtectedRoute = ({ children, requiredRoles }) => {
  const { user, isLoading } = useAuth()
  const location = useLocation()

  if (isLoading) return <div>加载中...</div>

  if (!user) {
    // 未登录:重定向到登录页,记录来源
    return <Navigate to="/login" state={{ from: location }} replace />
  }

  if (requiredRoles && !requiredRoles.some(role => user.roles.includes(role))) {
    // 无权限
    return <Navigate to="/403" replace />
  }

  return children
}

// 使用(路由配置中)
const router = createBrowserRouter([
  {
    path: '/dashboard',
    element: (
      <ProtectedRoute requiredRoles={['admin']}>
        <Dashboard />
      </ProtectedRoute>
    )
  }
])

// ========== 2. 使用 loader 做路由守卫(v6.4+ 推荐) ==========
const router = createBrowserRouter([
  {
    path: '/dashboard',
    loader: async () => {
      const user = await getUser()
      if (!user) throw redirect('/login')
      if (!user.roles.includes('admin')) throw redirect('/403')
      return { user }
    },
    element: <Dashboard />,
  }
])

// 在组件中使用 loader 返回的数据
import { useLoaderData } from 'react-router-dom'
const Dashboard = () => {
  const { user } = useLoaderData()
  return <h1>欢迎,{user.name}</h1>
}

// ========== 3. 登录后重定向回原页面 ==========
const LoginPage = () => {
  const navigate = useNavigate()
  const location = useLocation()
  const from = location.state?.from?.pathname || '/'

  const handleLogin = async (credentials) => {
    await login(credentials)
    navigate(from, { replace: true })  // 登录成功后跳回原页面
  }

  return <LoginForm onSubmit={handleLogin} />
}

// ========== 4. 路由元数据(自定义 handle) ==========
const router = createBrowserRouter([
  {
    path: '/admin',
    element: <Admin />,
    handle: {
      crumb: () => '管理后台',  // 面包屑
      title: '管理后台',        // 页面标题
      requireAuth: true,
    },
    children: [
      {
        path: 'users',
        element: <AdminUsers />,
        handle: { crumb: () => '用户管理', title: '用户管理' }
      }
    ]
  }
])

// 读取路由元数据
import { useMatches } from 'react-router-dom'
const Breadcrumb = () => {
  const matches = useMatches()
  const crumbs = matches.filter(m => m.handle?.crumb).map(m => m.handle.crumb())
  return <nav>{crumbs.join(' > ')}</nav>
}

💡 面试加分点: React Router v6.4 引入的 loader + action 是借鉴 Remix 的数据路由——先加载数据再渲染组件,避免了组件内部 useEffect 请求造成的瀑布流。路由守卫推荐用 loader + redirect 实现,比高阶组件更优雅。


15. Redux Toolkit 深入使用

createAsyncThunk(异步 Action)

import { createSlice, createAsyncThunk, configureStore } from '@reduxjs/toolkit'

// ========== 1. 定义异步 Thunk ==========
const fetchUsers = createAsyncThunk(
  'users/fetchUsers',
  async ({ page, pageSize }, { rejectWithValue, getState }) => {
    try {
      const response = await fetch(`/api/users?page=${page}&size=${pageSize}`)
      if (!response.ok) throw new Error('请求失败')
      return await response.json()
    } catch (error) {
      return rejectWithValue(error.message)  // 自定义错误信息
    }
  },
  {
    // 条件执行:如果已经在加载中,不重复请求
    condition: (_, { getState }) => {
      const { users } = getState()
      return users.status !== 'loading'
    }
  }
)

const deleteUser = createAsyncThunk('users/delete', async (userId, { dispatch }) => {
  await fetch(`/api/users/${userId}`, { method: 'DELETE' })
  dispatch(fetchUsers({ page: 1, pageSize: 10 }))  // 删除后刷新列表
  return userId
})

// ========== 2. Slice 处理异步状态 ==========
const usersSlice = createSlice({
  name: 'users',
  initialState: {
    list: [],
    total: 0,
    status: 'idle',  // 'idle' | 'loading' | 'succeeded' | 'failed'
    error: null,
  },
  reducers: {
    clearUsers: (state) => { state.list = []; state.total = 0 },
  },
  // extraReducers 处理 createAsyncThunk 生成的 action
  extraReducers: (builder) => {
    builder
      .addCase(fetchUsers.pending, (state) => {
        state.status = 'loading'
        state.error = null
      })
      .addCase(fetchUsers.fulfilled, (state, action) => {
        state.status = 'succeeded'
        state.list = action.payload.data
        state.total = action.payload.total
      })
      .addCase(fetchUsers.rejected, (state, action) => {
        state.status = 'failed'
        state.error = action.payload || action.error.message
      })
  }
})

// ========== 3. 组件中使用 ==========
const UserList = () => {
  const dispatch = useDispatch()
  const { list, status, error, total } = useSelector(state => state.users)

  useEffect(() => {
    dispatch(fetchUsers({ page: 1, pageSize: 10 }))
  }, [dispatch])

  if (status === 'loading') return <Spinner />
  if (status === 'failed') return <ErrorMsg message={error} />

  return (
    <ul>
      {list.map(user => (
        <li key={user.id}>
          {user.name}
          <button onClick={() => dispatch(deleteUser(user.id))}>删除</button>
        </li>
      ))}
    </ul>
  )
}

RTK Query(数据请求利器)

import { createApi, fetchBaseQuery } from '@reduxjs/toolkit/query/react'

// ========== 定义 API ==========
const userApi = createApi({
  reducerPath: 'userApi',
  baseQuery: fetchBaseQuery({
    baseUrl: '/api',
    prepareHeaders: (headers, { getState }) => {
      const token = getState().auth.token
      if (token) headers.set('Authorization', `Bearer ${token}`)
      return headers
    }
  }),
  tagTypes: ['User'],  // 缓存标签
  endpoints: (builder) => ({
    // 查询
    getUsers: builder.query({
      query: (params) => ({ url: '/users', params }),
      providesTags: (result) =>
        result ? [...result.map(({ id }) => ({ type: 'User', id })), 'User'] : ['User'],
    }),
    getUserById: builder.query({
      query: (id) => `/users/${id}`,
      providesTags: (result, error, id) => [{ type: 'User', id }],
    }),
    // 修改
    addUser: builder.mutation({
      query: (newUser) => ({ url: '/users', method: 'POST', body: newUser }),
      invalidatesTags: ['User'],  // 添加后自动刷新列表
    }),
    updateUser: builder.mutation({
      query: ({ id, ...data }) => ({ url: `/users/${id}`, method: 'PUT', body: data }),
      invalidatesTags: (result, error, { id }) => [{ type: 'User', id }],
    }),
    deleteUser: builder.mutation({
      query: (id) => ({ url: `/users/${id}`, method: 'DELETE' }),
      invalidatesTags: (result, error, id) => [{ type: 'User', id }],
    }),
  }),
})

export const {
  useGetUsersQuery, useGetUserByIdQuery,
  useAddUserMutation, useUpdateUserMutation, useDeleteUserMutation
} = userApi

// ========== 组件中使用 ==========
const UserList = () => {
  const { data: users, isLoading, error, refetch } = useGetUsersQuery({ page: 1 })
  const [deleteUser, { isLoading: isDeleting }] = useDeleteUserMutation()

  if (isLoading) return <Spinner />
  if (error) return <ErrorMsg />

  return (
    <ul>
      {users?.map(user => (
        <li key={user.id}>
          {user.name}
          <button onClick={() => deleteUser(user.id)} disabled={isDeleting}>删除</button>
        </li>
      ))}
    </ul>
  )
}

// store 配置
const store = configureStore({
  reducer: {
    [userApi.reducerPath]: userApi.reducer,
    users: usersSlice.reducer,
  },
  middleware: (getDefault) => getDefault().concat(userApi.middleware),
})

💡 面试加分点: RTK Query 类似 TanStack Query——自动缓存、自动去重、自动刷新、乐观更新。invalidatesTags + providesTags 实现了声明式缓存失效——修改数据后自动刷新相关查询。与 TanStack Query 不同的是,RTK Query 内置在 Redux 中,适合已使用 Redux 的项目。


16. React 的合成事件(SyntheticEvent)

对比项原生 DOM 事件React 合成事件
事件绑定绑定到具体 DOM委托到 root 节点(React 17+)
事件对象原生 EventSyntheticEvent(跨浏览器包装)
事件池—React 16 有事件池(17 移除)
阻止冒泡e.stopPropagation()只阻止 React 事件冒泡
命名onclickonClick(驼峰)
const EventDemo = () => {
  // ========== 事件处理 ==========
  const handleClick = (e) => {
    console.log(e)               // SyntheticEvent(合成事件对象)
    console.log(e.nativeEvent)   // 原生事件对象
    console.log(e.target)        // 触发事件的元素
    console.log(e.currentTarget) // 绑定事件的元素
  }

  // ========== 事件传参 ==========
  const handleDelete = (id, e) => {
    e.stopPropagation()
    console.log('删除:', id)
  }

  // ========== 表单事件 ==========
  const handleSubmit = (e) => {
    e.preventDefault()  // 阻止表单默认提交
    // 处理提交逻辑
  }

  return (
    <form onSubmit={handleSubmit}>
      <button onClick={handleClick}>点击</button>
      <button onClick={(e) => handleDelete(123, e)}>删除</button>
    </form>
  )
}

// ========== React 事件委托变化 ==========
// React 16:事件委托到 document
// React 17+:事件委托到 root 节点(createRoot 挂载点)
// 好处:多个 React 应用共存时不会互相干扰

// ========== 合成事件和原生事件的执行顺序 ==========
// 1. 原生事件(捕获阶段)
// 2. 原生事件(冒泡阶段)
// 3. React 合成事件(捕获阶段)
// 4. React 合成事件(冒泡阶段)
// 注意:React 17+ 将合成事件绑定到 root 而不是 document,所以顺序略有变化

// ⚠️ 混用原生事件和合成事件时,e.stopPropagation() 只阻止同类型事件
// React 的 stopPropagation 不会阻止原生事件冒泡

💡 面试加分点: React 17 把事件委托从 document 改到了根节点——这是为了支持微前端等多个 React 实例共存的场景。React 的 e.stopPropagation() 只能阻止 React 合成事件冒泡,不能阻止原生事件冒泡——如果需要,得用 e.nativeEvent.stopImmediatePropagation()。


17. 受控组件 vs 非受控组件

对比项受控组件非受控组件
数据管理React state 驱动DOM 自身管理
取值方式value + onChangeref.current.value
实时验证✅ 每次输入都可验证❌ 需要手动触发
性能每次输入触发重渲染✅ 不触发重渲染
推荐场景表单验证、联动文件上传、简单表单
// ========== 受控组件 ==========
const ControlledForm = () => {
  const [form, setForm] = useState({ name: '', email: '', age: '' })
  const [errors, setErrors] = useState({})

  const handleChange = (e) => {
    const { name, value } = e.target
    setForm(prev => ({ ...prev, [name]: value }))
    // ✅ 实时验证
    if (name === 'email' && value && !/\S+@\S+\.\S+/.test(value)) {
      setErrors(prev => ({ ...prev, email: '邮箱格式不正确' }))
    } else {
      setErrors(prev => ({ ...prev, [name]: '' }))
    }
  }

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log(form)
  }

  return (
    <form onSubmit={handleSubmit}>
      <input name="name" value={form.name} onChange={handleChange} />
      <input name="email" value={form.email} onChange={handleChange} />
      {errors.email && <span className="error">{errors.email}</span>}
      <button type="submit">提交</button>
    </form>
  )
}

// ========== 非受控组件 ==========
const UncontrolledForm = () => {
  const nameRef = useRef()
  const fileRef = useRef()

  const handleSubmit = (e) => {
    e.preventDefault()
    console.log('Name:', nameRef.current.value)
    console.log('File:', fileRef.current.files[0])
  }

  return (
    <form onSubmit={handleSubmit}>
      <input ref={nameRef} defaultValue="默认值" />  {/* defaultValue 而非 value */}
      <input ref={fileRef} type="file" />  {/* 文件输入只能非受控 */}
      <button type="submit">提交</button>
    </form>
  )
}

// ========== 推荐:使用表单库(React Hook Form) ==========
import { useForm } from 'react-hook-form'

const FormWithLib = () => {
  const { register, handleSubmit, formState: { errors } } = useForm()
  const onSubmit = (data) => console.log(data)

  return (
    <form onSubmit={handleSubmit(onSubmit)}>
      <input {...register('name', { required: '姓名必填' })} />
      {errors.name && <span>{errors.name.message}</span>}

      <input {...register('email', {
        required: '邮箱必填',
        pattern: { value: /\S+@\S+\.\S+/, message: '邮箱格式不正确' }
      })} />
      {errors.email && <span>{errors.email.message}</span>}

      <button type="submit">提交</button>
    </form>
  )
}

💡 面试加分点: <input type="file"> 只能是非受控组件(无法通过 React 设置 value)。生产中推荐 React Hook Form——它基于非受控组件 + ref,性能比 Formik(受控组件)好很多,表单字段变化不会触发整个表单重渲染。


18. React 组件的生命周期(类组件 vs 函数组件)

类组件生命周期:
┌─────────────────────────────────────────────────┐
│ 挂载阶段                                         │
│  constructor → getDerivedStateFromProps →        │
│  render → componentDidMount                     │
├─────────────────────────────────────────────────┤
│ 更新阶段                                         │
│  getDerivedStateFromProps → shouldComponentUpdate│
│  → render → getSnapshotBeforeUpdate →           │
│  componentDidUpdate                              │
├─────────────────────────────────────────────────┤
│ 卸载阶段                                         │
│  componentWillUnmount                            │
├─────────────────────────────────────────────────┤
│ 错误处理                                         │
│  getDerivedStateFromError → componentDidCatch    │
└─────────────────────────────────────────────────┘
类组件生命周期函数组件 Hook 对应
constructoruseState 惰性初始化
getDerivedStateFromPropsuseState + 渲染期间更新
render函数体本身
componentDidMountuseEffect(() => {}, [])
componentDidUpdateuseEffect(() => {}, [deps])
componentWillUnmountuseEffect 返回的清理函数
shouldComponentUpdateReact.memo
getSnapshotBeforeUpdate无直接对应(用 useLayoutEffect)
componentDidCatch无(仍需类组件)
// ========== 函数组件等价实现 ==========
const FunctionalLifecycle = ({ id }) => {
  // constructor 等价
  const [data, setData] = useState(() => expensiveInit())

  // componentDidMount 等价
  useEffect(() => {
    console.log('挂载')
    return () => console.log('卸载')  // componentWillUnmount 等价
  }, [])

  // componentDidUpdate 等价(跳过首次)
  const isFirst = useRef(true)
  useEffect(() => {
    if (isFirst.current) {
      isFirst.current = false
      return
    }
    console.log('id 更新为:', id)
  }, [id])

  // render
  return <div>{data}</div>
}

💡 面试加分点: React 16 废弃了 componentWillMount、componentWillReceiveProps、componentWillUpdate(加了 UNSAFE_ 前缀),因为它们在 Fiber 的 Render 阶段可能被多次调用。函数组件 + Hooks 是 React 的未来,唯一还需要类组件的地方是错误边界。


19. React 中的 CSS 方案对比

方案作用域运行时TypeScript优缺点
CSS Modules✅ 局部❌ 无⚠️ 需声明零运行时,构建时处理
Tailwind CSS✅ 原子类❌ 无✅开发快,class 长
styled-components✅ 局部⚠️ 有✅动态样式,SSR 需配置
Emotion✅ 局部⚠️ 有✅类似 SC,更灵活
CSS-in-JS (零运行时)✅ 局部❌ 无✅vanilla-extract、Panda CSS
内联样式✅—✅无伪类/媒体查询
// ========== 1. CSS Modules ==========
// Button.module.css → .btn { color: red; } .btnPrimary { color: blue; }
import styles from './Button.module.css'
const Button = () => <button className={styles.btn}>按钮</button>
// 生成:<button class="Button_btn_x3j2k">

// ========== 2. Tailwind CSS ==========
const Button = () => (
  <button className="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
    按钮
  </button>
)

// ========== 3. styled-components ==========
import styled from 'styled-components'
const StyledButton = styled.button`
  background: ${props => props.$primary ? 'blue' : 'gray'};
  color: white;
  padding: 8px 16px;
  border-radius: 4px;
  &:hover { opacity: 0.8; }
`
const App = () => <StyledButton $primary>按钮</StyledButton>

// ========== 4. clsx/classnames 条件类名 ==========
import clsx from 'clsx'
const Button = ({ variant, size, disabled }) => (
  <button className={clsx(
    'btn',
    variant === 'primary' && 'btn-primary',
    variant === 'danger' && 'btn-danger',
    size === 'large' && 'btn-lg',
    disabled && 'btn-disabled'
  )}>
    按钮
  </button>
)

💡 面试加分点: 2024-2025 的趋势:Tailwind CSS + CSS Modules 组合最流行——Tailwind 处理大部分样式,CSS Modules 处理复杂/动态样式。CSS-in-JS 运行时方案(SC/Emotion)因性能问题(运行时生成样式)逐渐被零运行时方案(vanilla-extract、Panda CSS)替代。


20. React 19 新特性

特性说明
React Compiler自动记忆化,不再需要手写 useMemo/useCallback
Actions表单处理新范式(useActionState、useFormStatus)
use()在条件/循环中读取 Promise 和 Context
ref 作为 prop不再需要 forwardRef
文档元数据组件中直接写 <title>、<meta>
样式支持<link> 样式优先级/去重
Server ComponentsRSC 正式稳定
// ========== 1. use() Hook:在条件中读取 Promise/Context ==========
import { use, Suspense } from 'react'

function UserProfile({ userPromise }) {
  // 可以在 if/循环中使用(打破了 Hook 规则限制)
  const user = use(userPromise)  // 自动配合 Suspense
  return <h1>{user.name}</h1>
}

// 条件读取 Context
function ThemeText({ showTheme }) {
  if (showTheme) {
    const theme = use(ThemeContext)  // ✅ 可以在条件中使用
    return <p>当前主题: {theme}</p>
  }
  return <p>主题已隐藏</p>
}

// ========== 2. useActionState(表单处理) ==========
import { useActionState } from 'react'

function LoginForm() {
  const [state, formAction, isPending] = useActionState(
    async (prevState, formData) => {
      const email = formData.get('email')
      const password = formData.get('password')
      try {
        await login(email, password)
        return { success: true }
      } catch (error) {
        return { error: error.message }
      }
    },
    { error: null, success: false }
  )

  return (
    <form action={formAction}>
      <input name="email" type="email" />
      <input name="password" type="password" />
      {state.error && <p className="error">{state.error}</p>}
      <button disabled={isPending}>{isPending ? '登录中...' : '登录'}</button>
    </form>
  )
}

// ========== 3. useFormStatus(子组件获取表单状态) ==========
import { useFormStatus } from 'react-dom'

function SubmitButton() {
  const { pending, data, method } = useFormStatus()
  return <button disabled={pending}>{pending ? '提交中...' : '提交'}</button>
}

// ========== 4. useOptimistic(乐观更新) ==========
import { useOptimistic } from 'react'

function TodoList({ todos, addTodo }) {
  const [optimisticTodos, addOptimistic] = useOptimistic(
    todos,
    (state, newTodo) => [...state, { ...newTodo, pending: true }]
  )

  const handleAdd = async (formData) => {
    const newTodo = { text: formData.get('text'), id: Date.now() }
    addOptimistic(newTodo)  // 立即显示(乐观更新)
    await addTodo(newTodo)   // 实际请求
  }

  return (
    <div>
      <form action={handleAdd}>
        <input name="text" />
        <button type="submit">添加</button>
      </form>
      {optimisticTodos.map(todo => (
        <p key={todo.id} style={{ opacity: todo.pending ? 0.5 : 1 }}>{todo.text}</p>
      ))}
    </div>
  )
}

// ========== 5. ref 作为普通 prop ==========
// React 19 之前
const Input = forwardRef((props, ref) => <input ref={ref} {...props} />)
// React 19
function Input({ ref, ...props }) {
  return <input ref={ref} {...props} />
}

// ========== 6. 文档元数据 ==========
function BlogPost({ post }) {
  return (
    <article>
      <title>{post.title}</title>
      <meta name="description" content={post.summary} />
      <h1>{post.title}</h1>
      <p>{post.content}</p>
    </article>
  )
}

💡 面试加分点: React 19 最大的变革是 React Compiler——它在编译时自动完成记忆化(等效于 useMemo/useCallback/memo),开发者不再需要手动优化。use() 打破了「Hook 不能在条件/循环中调用」的限制。useActionState 是 React 官方对表单处理的标准答案。


21. TanStack Query(React Query)

TanStack Query 是管理服务端状态(Server State)的最佳方案。

特性说明
自动缓存相同 queryKey 的请求自动复用缓存
后台刷新窗口聚焦时自动重新请求
智能重试请求失败自动重试(默认 3 次)
乐观更新先更新 UI,失败后回滚
分页/无限滚动内置 useInfiniteQuery
并行/依赖查询自动处理查询依赖关系
import { useQuery, useMutation, useQueryClient, QueryClient, QueryClientProvider } from '@tanstack/react-query'

// ========== 配置 ==========
const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000,    // 5 分钟内不重新请求
      gcTime: 10 * 60 * 1000,      // 10 分钟后清除缓存(原 cacheTime)
      retry: 2,                     // 失败重试 2 次
      refetchOnWindowFocus: true,   // 窗口聚焦时刷新
    }
  }
})
const App = () => (
  <QueryClientProvider client={queryClient}><Root /></QueryClientProvider>
)

// ========== 查询 ==========
function UserList() {
  const { data, isLoading, isError, error, refetch, isFetching } = useQuery({
    queryKey: ['users', { page: 1 }],  // 唯一标识(自动序列化)
    queryFn: ({ queryKey }) => fetch(`/api/users?page=${queryKey[1].page}`).then(r => r.json()),
    staleTime: 30 * 1000,  // 30 秒内不重新请求
    placeholderData: previousData,  // 切换参数时保留旧数据
  })

  if (isLoading) return <Spinner />
  if (isError) return <p>错误: {error.message}</p>
  return <ul>{data.map(u => <li key={u.id}>{u.name}</li>)}</ul>
}

// ========== 依赖查询 ==========
function UserPosts({ userId }) {
  const userQuery = useQuery({
    queryKey: ['user', userId],
    queryFn: () => fetchUser(userId),
  })
  const postsQuery = useQuery({
    queryKey: ['posts', userId],
    queryFn: () => fetchPosts(userId),
    enabled: !!userQuery.data,  // 等用户数据加载完再查询文章
  })
}

// ========== 修改(乐观更新) ==========
function useUpdateUser() {
  const queryClient = useQueryClient()

  return useMutation({
    mutationFn: (updatedUser) => fetch(`/api/users/${updatedUser.id}`, {
      method: 'PUT', body: JSON.stringify(updatedUser)
    }),
    // 乐观更新
    onMutate: async (newUser) => {
      await queryClient.cancelQueries({ queryKey: ['users'] })
      const previous = queryClient.getQueryData(['users'])
      queryClient.setQueryData(['users'], (old) =>
        old.map(u => u.id === newUser.id ? { ...u, ...newUser } : u)
      )
      return { previous }  // 返回 context
    },
    onError: (err, newUser, context) => {
      queryClient.setQueryData(['users'], context.previous)  // 回滚
    },
    onSettled: () => {
      queryClient.invalidateQueries({ queryKey: ['users'] })  // 无论成败都刷新
    }
  })
}

// ========== 无限滚动 ==========
import { useInfiniteQuery } from '@tanstack/react-query'
function InfiniteList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteQuery({
    queryKey: ['posts'],
    queryFn: ({ pageParam = 1 }) => fetchPosts(pageParam),
    getNextPageParam: (lastPage) => lastPage.nextCursor || undefined,
  })

  return (
    <div>
      {data?.pages.map(page => page.items.map(item => <div key={item.id}>{item.title}</div>))}
      {hasNextPage && (
        <button onClick={() => fetchNextPage()} disabled={isFetchingNextPage}>
          {isFetchingNextPage ? '加载中...' : '加载更多'}
        </button>
      )}
    </div>
  )
}

💡 面试加分点: TanStack Query 解决了前端最大的痛点——服务端状态管理(缓存、去重、后台刷新、乐观更新、分页)。与 Redux 的区别:Redux 管理客户端状态(UI 状态、用户偏好),TanStack Query 管理服务端状态(API 数据)。queryKey 是唯一标识,改变它等于发起新请求。


22. SSR 和 Next.js

渲染方式说明首屏速度SEO适用场景
CSR客户端渲染慢❌后台管理系统
SSR服务端渲染✅ 快✅动态内容(电商、新闻)
SSG静态生成✅✅ 最快✅博客、文档、营销页
ISR增量静态生成✅✅ 快✅数据定期更新的页面
RSCReact Server Components✅✅✅数据密集型页面
// ========== Next.js App Router(推荐) ==========
// app/page.tsx → /
// app/about/page.tsx → /about
// app/users/[id]/page.tsx → /users/:id
// app/blog/[...slug]/page.tsx → /blog/a/b/c

// ========== Server Component(默认,在服务端渲染) ==========
// app/users/page.tsx
async function UsersPage() {
  // 直接在组件中 await(无需 useEffect!)
  const users = await fetch('https://api.example.com/users').then(r => r.json())

  return (
    <ul>
      {users.map(user => <li key={user.id}>{user.name}</li>)}
    </ul>
  )
}
// Server Component 优势:
// ✅ 不发送 JS 到客户端(减小包体积)
// ✅ 直接访问数据库/文件系统
// ✅ 自动代码分割

// ========== Client Component(交互组件) ==========
'use client'  // 必须声明
import { useState } from 'react'

export function Counter() {
  const [count, setCount] = useState(0)
  return <button onClick={() => setCount(c => c + 1)}>{count}</button>
}

// ========== 数据获取模式 ==========
// Server Component 中直接 async/await
async function Page({ params }) {
  const post = await db.post.findUnique({ where: { id: params.id } })
  return <article>{post.title}</article>
}

// ========== Metadata(SEO) ==========
// app/page.tsx
export const metadata = {
  title: '首页',
  description: '网站描述',
  openGraph: { title: '分享标题', image: '/og.png' },
}

// 动态 Metadata
export async function generateMetadata({ params }) {
  const post = await getPost(params.id)
  return { title: post.title, description: post.excerpt }
}

// ========== 路由分组和布局 ==========
// app/(marketing)/layout.tsx → 营销页面布局
// app/(dashboard)/layout.tsx → 后台布局
// (xxx) 括号目录不影响 URL

// ========== Server Actions ==========
// app/actions.ts
'use server'
export async function createUser(formData) {
  const name = formData.get('name')
  await db.user.create({ data: { name } })
  revalidatePath('/users')  // 重新验证页面缓存
}

// app/create-user/page.tsx
import { createUser } from './actions'
export default function CreateUserPage() {
  return (
    <form action={createUser}>
      <input name="name" />
      <button type="submit">创建</button>
    </form>
  )
}

💡 面试加分点: Next.js App Router 引入的 React Server Components (RSC) 是 React 的范式转变——组件默认在服务端渲染,只有需要交互的组件才添加 'use client'。Server Component 的 JS 不会发送到客户端,可以直接访问数据库——这让 React 从纯前端框架变成了全栈框架。


23. React 的闭包陷阱详解

什么是闭包陷阱?

函数组件每次渲染都会创建新的闭包,如果闭包中捕获了旧的 state/props 值,就会出现"读到旧值"的问题。这是 React 函数组件最常见的 Bug。

// ========== 经典闭包陷阱:setTimeout 读到旧值 ==========
function Counter() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    // ❌ 闭包陷阱!
    setTimeout(() => {
      // 这个 count 是点击时的值(闭包捕获),不是 3 秒后的最新值!
      console.log('count:', count)  // 永远打印点击那一刻的 count
      setCount(count + 1)           // 基于旧值更新,可能不对
    }, 3000)
  }

  // 场景:count=0,快速点击 3 次
  // 预期:0, 1, 2 → count = 3
  // 实际:0, 0, 0 → count = 1(三次 setTimeout 都捕获了 count=0)

  return (
    <div>
      <p>{count}</p>
      <button onClick={handleClick}>+1 (3秒后)</button>
    </div>
  )
}

解决方案大全

// ========== 解决方案1:函数式更新(解决 setState 读旧值) ==========
function Counter() {
  const [count, setCount] = useState(0)

  const handleClick = () => {
    setTimeout(() => {
      // ✅ 函数式更新:不依赖闭包中的 count,而是基于最新 state 更新
      setCount(prev => prev + 1)
    }, 3000)
  }
  // 快速点击 3 次 → count = 3 ✅

  return <button onClick={handleClick}>{count}</button>
}

// ========== 解决方案2:useRef 读最新值(解决"读"旧值的问题) ==========
function Counter() {
  const [count, setCount] = useState(0)
  const countRef = useRef(count)

  // 每次渲染同步 ref
  useEffect(() => {
    countRef.current = count
  })

  const handleClick = () => {
    setTimeout(() => {
      // ✅ ref.current 永远是最新值
      console.log('最新 count:', countRef.current)
    }, 3000)
  }

  return <button onClick={handleClick}>{count}</button>
}

// ========== 解决方案3:useRef 保存回调(避免 useEffect 依赖过多) ==========
function useInterval(callback, delay) {
  const savedCallback = useRef(callback)

  // 每次渲染更新 ref(不需要重启定时器)
  useEffect(() => {
    savedCallback.current = callback
  })

  useEffect(() => {
    const id = setInterval(() => {
      savedCallback.current()  // ✅ 永远调用最新的回调
    }, delay)
    return () => clearInterval(id)
  }, [delay])  // ✅ 只在 delay 变化时重启定时器
}

常见闭包陷阱场景

// ========== 场景1:useEffect 依赖遗漏 ==========
function SearchBox() {
  const [query, setQuery] = useState('')
  const [results, setResults] = useState([])

  useEffect(() => {
    // ❌ 闭包陷阱:如果不加 query 到依赖数组
    // 这个 effect 只在 mount 时运行一次,query 永远是 ''
    const handler = setTimeout(() => {
      fetchResults(query).then(setResults)
    }, 300)
    return () => clearTimeout(handler)
  }, [query])  // ✅ 必须加上 query

  return <input value={query} onChange={e => setQuery(e.target.value)} />
}

// ========== 场景2:事件监听器捕获旧 state ==========
function ScrollTracker() {
  const [scrollY, setScrollY] = useState(0)
  const [isTop, setIsTop] = useState(true)

  useEffect(() => {
    const handleScroll = () => {
      setScrollY(window.scrollY)
      // ❌ 这里的 scrollY 是 useEffect 执行时的旧值
      // console.log(scrollY)  // 永远是 0
      // ✅ 用函数式更新或直接读 window.scrollY
      setIsTop(window.scrollY < 100)
    }
    window.addEventListener('scroll', handleScroll)
    return () => window.removeEventListener('scroll', handleScroll)
  }, [])  // 空依赖:只绑定一次,闭包捕获的是初始值

  return <p>scrollY: {scrollY}, isTop: {String(isTop)}</p>
}

// ========== 场景3:useCallback 的依赖与闭包 ==========
function Parent() {
  const [count, setCount] = useState(0)

  // ❌ 空依赖:handleClick 永远捕获 count=0
  const handleClick = useCallback(() => {
    console.log(count)  // 永远是 0
  }, [])

  // ✅ 方案1:加上 count 依赖(但每次 count 变化都创建新函数)
  const handleClick1 = useCallback(() => {
    console.log(count)
  }, [count])

  // ✅ 方案2:useRef(不需要 count 依赖,函数引用稳定)
  const countRef = useRef(count)
  countRef.current = count
  const handleClick2 = useCallback(() => {
    console.log(countRef.current)  // 永远是最新值
  }, [])

  return <MemoChild onClick={handleClick2} />
}

闭包陷阱速查表

// ========== 闭包陷阱场景和解决方案速查 ==========

| 场景                     | 问题                        | 解决方案                          |
|--------------------------|---------------------------|-----------------------------------|
| setTimeout/setInterval   | 回调读到旧 state            | 函数式 setState / useRef          |
| useEffect 空依赖         | effect 内读到初始 state      | 加依赖 / useRef                   |
| useCallback 空依赖       | 回调读到旧 state            | 加依赖 / useRef                   |
| 事件监听器               | handler 读到旧 state        | useRef / 函数式更新               |
| Promise/async            | .then 中读到旧 state        | useRef / 函数式更新               |

💡 面试加分点: 闭包陷阱的根本原因是函数组件的 Capture Value 特性——每次渲染都是一个独立的闭包快照。解决思路只有两个方向:1)不依赖闭包值(函数式更新 setState(prev => ...) );2)用 ref 逃逸闭包(useRef 的 .current 是可变引用,不受闭包影响)。React 19 的 React Compiler 可以自动处理部分闭包问题,但理解底层原理仍然是面试必考。


24. React 的批量更新(Batching)机制

什么是批量更新?

批量更新是指 React 将多次 setState 合并为一次渲染,避免不必要的中间状态渲染。

// ========== React 17 的批量更新行为 ==========
function Counter() {
  const [count, setCount] = useState(0)
  const [flag, setFlag] = useState(false)

  // ✅ React 17 事件处理函数中:自动批量(只渲染一次)
  const handleClick = () => {
    setCount(c => c + 1)  // 不会立即渲染
    setFlag(f => !f)       // 不会立即渲染
    // → 两次更新合并,组件只渲染一次 ✅
  }

  // ❌ React 17 在 setTimeout/Promise 中:不批量!(渲染两次)
  const handleAsync = () => {
    setTimeout(() => {
      setCount(c => c + 1)  // 立即渲染一次!
      setFlag(f => !f)       // 又渲染一次!
      // → 组件渲染了两次 ❌
    }, 0)
  }

  // ❌ React 17 在 fetch 回调中:不批量!
  const handleFetch = () => {
    fetch('/api/data').then(() => {
      setCount(c => c + 1)  // 渲染一次
      setFlag(f => !f)       // 又渲染一次
    })
  }

  console.log('render')  // 观察渲染次数
  return <button onClick={handleClick}>{count}</button>
}

React 18 的自动批量更新(Automatic Batching)

// ========== React 18:所有地方都自动批量! ==========
import { createRoot } from 'react-dom/client'

// 前提:必须使用 createRoot(不是 ReactDOM.render)
const root = createRoot(document.getElementById('root'))
root.render(<App />)

function Counter() {
  const [count, setCount] = useState(0)
  const [flag, setFlag] = useState(false)

  // ✅ 事件处理函数:批量(React 17 也支持)
  const handleClick = () => {
    setCount(c => c + 1)
    setFlag(f => !f)
    // → 只渲染一次 ✅
  }

  // ✅ setTimeout:也批量了!(React 18 新特性)
  const handleTimeout = () => {
    setTimeout(() => {
      setCount(c => c + 1)
      setFlag(f => !f)
      // → 只渲染一次 ✅(React 17 中会渲染两次)
    }, 0)
  }

  // ✅ Promise:也批量了!
  const handleFetch = () => {
    fetch('/api/data').then(() => {
      setCount(c => c + 1)
      setFlag(f => !f)
      // → 只渲染一次 ✅
    })
  }

  // ✅ 原生事件监听器:也批量了!
  useEffect(() => {
    const handler = () => {
      setCount(c => c + 1)
      setFlag(f => !f)
      // → 只渲染一次 ✅
    }
    document.addEventListener('click', handler)
    return () => document.removeEventListener('click', handler)
  }, [])

  console.log('render')
  return <button onClick={handleClick}>{count}</button>
}

flushSync:强制同步更新

// ========== 跳出批量更新:flushSync ==========
import { flushSync } from 'react-dom'

function Counter() {
  const [count, setCount] = useState(0)
  const [flag, setFlag] = useState(false)

  const handleClick = () => {
    // 强制同步更新(不批量)
    flushSync(() => {
      setCount(c => c + 1)
    })
    // 此时 DOM 已经更新了!count 已经变了
    console.log('DOM 已更新')

    flushSync(() => {
      setFlag(f => !f)
    })
    // 此时 DOM 又更新了一次

    // 总共渲染了 2 次(而不是批量的 1 次)
  }

  // ========== flushSync 的实际应用场景 ==========
  // 场景:需要在两次状态更新之间读取 DOM
  const handleAdd = () => {
    flushSync(() => {
      setItems(prev => [...prev, newItem])
    })
    // DOM 已经更新,新元素已渲染
    listRef.current.lastChild.scrollIntoView()  // ✅ 滚动到新元素
  }

  return <button onClick={handleClick}>{count}</button>
}

批量更新原理(简化)

// ========== React 批量更新核心流程 ==========

// React 17:基于"执行上下文"标记
// 进入事件处理 → 设置 isBatchingUpdates = true
// setState → 收集到更新队列(不立即处理)
// 离开事件处理 → isBatchingUpdates = false → flush 队列 → 一次渲染
// 问题:setTimeout/Promise 的回调执行时,已经不在事件处理上下文中

// React 18:基于"优先级调度" + "自动包裹"
// 所有 setState 都进入 Fiber 的更新队列
// 在同一个微任务中的所有更新自动合并
// 通过 Scheduler 统一调度,一次处理所有待更新的 Fiber

// ========== 与 Vue 的批量更新对比 ==========
// Vue:基于响应式 + 异步更新队列
//   - 数据变化 → watcher 入队(去重)→ nextTick flush
//   - 从一开始就是自动批量的(任何场景)

// React 17:基于事件系统的"合成事件包裹"
//   - 只在 React 事件处理函数中批量
//   - setTimeout/Promise/原生事件中不批量

// React 18:基于 createRoot 的自动批量
//   - 所有场景都自动批量(和 Vue 一致了)
//   - 必须使用 createRoot(旧的 ReactDOM.render 不支持)

💡 面试加分点: React 18 的 Automatic Batching 是一个重大改进——将批量更新从"仅事件处理函数"扩展到"所有异步场景"。但前提是必须使用 createRoot API(ReactDOM.render 不支持)。flushSync 是逃生舱口,用于需要立即同步 DOM 更新的场景(极少使用)。面试时还可以对比 Vue 的批量更新机制——Vue 从一开始就是全场景自动批量的(异步更新队列 + watcher 去重)。


25. React Suspense 的完整用法

Suspense 的核心概念

<Suspense> 是 React 提供的声明式异步 UI 边界——它让你"等待"某个异步操作完成,在等待期间显示 fallback 内容。

支持场景React 版本说明
React.lazy 代码分割16.6+异步加载组件
Data Fetching18+配合支持 Suspense 的库(TanStack Query、Relay)
Server Components19+RSC 中的流式渲染
use() Hook19+直接在组件中读取 Promise
// ========== 1. 最基本的 Suspense:代码分割 ==========
import { lazy, Suspense } from 'react'

// lazy 返回一个"异步组件"
const Dashboard = lazy(() => import('./Dashboard'))
const Settings = lazy(() => import('./Settings'))

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Routes>
        <Route path="/dashboard" element={<Dashboard />} />
        <Route path="/settings" element={<Settings />} />
      </Routes>
    </Suspense>
  )
}

// ========== 2. 嵌套 Suspense ==========
function App() {
  return (
    // 外层 Suspense:页面级加载
    <Suspense fallback={<PageSkeleton />}>
      <Header />
      <main>
        {/* 内层 Suspense:模块级加载 */}
        <Suspense fallback={<CardSkeleton />}>
          <UserProfile />
        </Suspense>
        <Suspense fallback={<TableSkeleton />}>
          <OrderList />
        </Suspense>
      </main>
    </Suspense>
  )
  // 嵌套 Suspense 的好处:
  // - 外层加载完毕后显示 Header + 两个骨架屏
  // - UserProfile 和 OrderList 各自独立加载
  // - 某个模块失败不影响其他模块
}

React 19 的 use() + Suspense

// ========== 3. use() + Suspense(React 19) ==========
import { use, Suspense } from 'react'

// ✅ use() 可以在条件中使用(打破 Hook 规则限制)
function UserProfile({ userPromise }) {
  const user = use(userPromise)  // 自动"挂起"直到 Promise resolve
  return (
    <div>
      <h1>{user.name}</h1>
      <p>{user.email}</p>
    </div>
  )
}

// 父组件
function App() {
  // ✅ 在父组件创建 Promise(不是在子组件内部)
  const userPromise = fetchUser(userId)

  return (
    <Suspense fallback={<ProfileSkeleton />}>
      <UserProfile userPromise={userPromise} />
    </Suspense>
  )
}

// ⚠️ 注意:不要在组件内部创建 Promise
function BadExample() {
  // ❌ 每次渲染都创建新的 Promise → 无限循环!
  const user = use(fetch('/api/user').then(r => r.json()))
  return <div>{user.name}</div>
}

配合 TanStack Query 使用

// ========== 4. TanStack Query + Suspense ==========
import { useSuspenseQuery } from '@tanstack/react-query'

// ✅ useSuspenseQuery:数据加载期间自动挂起
function UserList() {
  // 不需要处理 isLoading/isError,Suspense 和 ErrorBoundary 接管
  const { data: users } = useSuspenseQuery({
    queryKey: ['users'],
    queryFn: () => fetch('/api/users').then(r => r.json())
  })

  // data 一定有值(不用判断 undefined)
  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  )
}

// ========== 配合 ErrorBoundary ==========
import { ErrorBoundary } from 'react-error-boundary'

function App() {
  return (
    <ErrorBoundary fallback={<ErrorMessage />}>
      <Suspense fallback={<LoadingSpinner />}>
        <UserList />
      </Suspense>
    </ErrorBoundary>
  )
  // 加载中 → <LoadingSpinner />
  // 加载成功 → <UserList />
  // 加载失败 → <ErrorMessage />(被 ErrorBoundary 捕获)
}

// ========== 5. SuspenseList(实验性,控制加载顺序) ==========
// React 未来可能稳定的 API
// <SuspenseList revealOrder="forwards">
//   <Suspense fallback={<Skeleton1 />}><Section1 /></Suspense>
//   <Suspense fallback={<Skeleton2 />}><Section2 /></Suspense>
//   <Suspense fallback={<Skeleton3 />}><Section3 /></Suspense>
// </SuspenseList>
// revealOrder="forwards" → 按顺序展示(即使后面的先加载完)
// revealOrder="together" → 全部加载完后同时展示

Suspense 原理(简化)

// ========== Suspense 的底层机制 ==========

// 1. 组件"挂起"时,会 throw 一个 Promise
// 2. Suspense 捕获这个 Promise
// 3. 渲染 fallback
// 4. Promise resolve 后,重新渲染子组件

// 简化版工作流程:
// try {
//   render(<AsyncComponent />)
// } catch (promise) {
//   if (promise instanceof Promise) {
//     // 渲染 fallback
//     render(<Fallback />)
//     // 等待 Promise 完成
//     promise.then(() => {
//       // 重新渲染子组件
//       render(<AsyncComponent />)
//     })
//   }
// }

// 这就是为什么 use() 和 React.lazy 能配合 Suspense:
// - React.lazy:加载组件的 import() 返回 Promise,未完成时 throw
// - use(promise):Promise 未 resolve 时 throw

💡 面试加分点: Suspense 的本质是声明式的异步 UI 边界——把"加载中/成功/失败"的状态管理从组件内部提升到了组件外部。最佳实践:<ErrorBoundary> 包裹 <Suspense>,分别处理错误和加载态。Suspense 的底层原理是"throw Promise"——子组件在数据未就绪时抛出 Promise,Suspense 捕获后显示 fallback,Promise resolve 后重新渲染。React 19 的 use() 让 Suspense 的使用更加直观。


26. React Server Components(RSC)原理

什么是 Server Components?

Server Components 是只在服务器上运行的 React 组件——它们在服务端渲染,生成的是一种特殊的序列化格式(RSC Payload),而不是 HTML。客户端接收后直接渲染,不需要下载组件的 JS 代码。

特性Server ComponentsClient Components传统 SSR
运行环境仅服务器服务器 + 客户端服务器渲染,客户端 Hydrate
JS 发送到客户端❌ 不发送✅ 需要发送✅ 需要发送
访问后端资源✅ 直接访问 DB/文件❌ 需要 API❌ 需要 API
useState/useEffect❌ 不支持✅ 支持✅ 支持
事件处理(onClick)❌ 不支持✅ 支持✅ 支持
包体积影响✅ 零影响计入 Bundle计入 Bundle
// ========== RSC 渲染流程 ==========

// 1. 请求到达服务器
// 2. Server Components 在服务器执行(可以直接读 DB、调内部 API)
// 3. 生成 RSC Payload(序列化的组件树 + 数据)
// 4. 流式传输到客户端
// 5. 客户端 React Runtime 接收 RSC Payload
// 6. 将 Server Components 渲染为 DOM
// 7. Client Components 在客户端 Hydrate(绑定事件等)

// 与传统 SSR 的区别:
// SSR:服务端生成 HTML → 客户端下载 JS → Hydrate(全量)
// RSC:服务端生成 RSC Payload → 只下载 Client Components 的 JS → 部分 Hydrate

"use client" 和 "use server"

// ========== 1. Server Component(默认,不需要标记) ==========
// app/page.tsx(Next.js App Router 中默认是 Server Component)

// ✅ 可以直接在组件中访问数据库
import { db } from '@/lib/db'

async function UserList() {
  // ✅ 直接查数据库(不需要 API 接口!)
  const users = await db.user.findMany()

  // ✅ 可以使用大型库(不会打包到客户端)
  import dayjs from 'dayjs'  // 不计入客户端 bundle

  return (
    <ul>
      {users.map(user => (
        <li key={user.id}>
          {user.name} - {dayjs(user.createdAt).format('YYYY-MM-DD')}
        </li>
      ))}
    </ul>
  )
}

// ========== 2. Client Component(用 "use client" 标记) ==========
// components/LikeButton.tsx
'use client'  // ⚠️ 必须在文件顶部(不是组件顶部)

import { useState } from 'react'

export function LikeButton({ initialCount }) {
  // ✅ Client Component 可以使用 Hooks
  const [count, setCount] = useState(initialCount)

  // ✅ 可以绑定事件
  return (
    <button onClick={() => setCount(c => c + 1)}>
      ❤️ {count}
    </button>
  )
}

// ========== 3. Server Component 中使用 Client Component ==========
// app/posts/[id]/page.tsx(Server Component)
import { db } from '@/lib/db'
import { LikeButton } from '@/components/LikeButton'  // Client Component

async function PostPage({ params }) {
  const post = await db.post.findUnique({ where: { id: params.id } })

  return (
    <article>
      <h1>{post.title}</h1>
      <p>{post.content}</p>
      {/* ✅ Server Component 可以渲染 Client Component */}
      {/* Server 侧将 initialCount 作为 props 传递 */}
      <LikeButton initialCount={post.likes} />
    </article>
  )
}

Server Actions("use server")

// ========== 4. Server Actions:从客户端调用服务端函数 ==========

// 方式1:独立文件
// app/actions.ts
'use server'

export async function createPost(formData: FormData) {
  const title = formData.get('title') as string
  const content = formData.get('content') as string

  // ✅ 在服务端执行:可以直接操作数据库
  await db.post.create({ data: { title, content } })

  // 重新验证缓存,触发页面更新
  revalidatePath('/posts')
}

// 方式2:内联在 Server Component 中
async function PostForm() {
  async function handleSubmit(formData: FormData) {
    'use server'  // 标记为 Server Action
    await db.post.create({
      data: { title: formData.get('title') }
    })
    revalidatePath('/posts')
  }

  return (
    <form action={handleSubmit}>
      <input name="title" />
      <button type="submit">发布</button>
    </form>
  )
}

// 在 Client Component 中使用 Server Action
'use client'
import { createPost } from './actions'
import { useActionState } from 'react'

function CreatePostForm() {
  const [state, formAction, isPending] = useActionState(createPost, null)

  return (
    <form action={formAction}>
      <input name="title" />
      <button disabled={isPending}>
        {isPending ? '发布中...' : '发布'}
      </button>
    </form>
  )
}

Server / Client 组件选型

// ========== 组件选型速查表 ==========

| 需求                          | Server Component | Client Component |
|-------------------------------|:---------------:|:---------------:|
| 获取数据(DB/API)              | ✅              | ❌(需要 useEffect) |
| 访问后端资源(文件系统等)        | ✅              | ❌              |
| 使用大型依赖(不影响 Bundle)     | ✅              | ❌              |
| 使用 Hooks(useState 等)       | ❌              | ✅              |
| 绑定事件(onClick 等)          | ❌              | ✅              |
| 使用浏览器 API(localStorage)  | ❌              | ✅              |
| 使用 Context                   | ❌              | ✅              |

// ========== 设计原则 ==========
// 1. 默认使用 Server Component(零 JS 发送到客户端)
// 2. 只在需要交互/Hooks/浏览器 API 时才用 Client Component
// 3. 将 Client Component 推到组件树的叶子节点(最小化客户端 JS)
// 4. Server Component 可以 import Client Component,反之不行
// 5. 可以将 Server Component 作为 children 传给 Client Component

// 模式:
// <ServerLayout>           ← Server(不发送 JS)
//   <Header />             ← Server
//   <SearchBar />          ← Client(有输入交互)
//   <ProductList>          ← Server(直接查 DB)
//     <AddToCartButton />  ← Client(有点击交互)
//   </ProductList>
// </ServerLayout>

💡 面试加分点: RSC 的核心价值是"零 Bundle 的服务端组件"——Server Components 不会发送任何 JS 到客户端,极大减少 Bundle 体积。与传统 SSR 的本质区别:SSR 是"先在服务端渲染 HTML,再在客户端全量 Hydrate",RSC 是"服务端组件只运行在服务端,客户端只 Hydrate Client Components"。设计原则是"默认 Server,按需 Client"——只有需要交互(事件/Hooks/浏览器 API)的组件才标记为 Client Component。Server Actions("use server")则解决了客户端直接调用服务端逻辑的问题,相当于自动生成了 API 接口。