React 高频面试题

React 高频面试题

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

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

核心概念 说明
JSX JavaScript 语法扩展,编译为 React.createElement() 调用
组件 UI 的基本构建单元,分函数组件和类组件
单向数据流 数据从父组件通过 props 向下流动
虚拟 DOM 内存中的轻量 DOM 表示,通过 diff 算法最小化真实 DOM 操作
声明式 描述 UI 应该是什么样子,而不是如何操作 DOM
jsx 复制代码
// ========== 函数组件(推荐) ==========
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 Drilling Context 状态管理库
层级限制 逐层传递 跨层级 跨层级
性能 ✅ 按需渲染 ⚠️ Provider value 变化时所有消费者重渲染 ✅ 精确更新
适用场景 1-2 层 主题/语言/认证等低频变化 高频更新的全局状态
jsx 复制代码
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?

jsx 复制代码
// ========== 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 中的工作原理

jsx 复制代码
// ========== 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 元素的"身份证"

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 ⚠️ 默认用 index React 会警告(开发环境),等价于 index
jsx 复制代码
// ========== 不同场景下的 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 的作用域:兄弟节点之间唯一即可

jsx 复制代码
// ========== 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 强制重建组件的进阶场景

jsx 复制代码
// ========== 场景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 相关的常见坑

jsx 复制代码
// ========== 坑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)

js 复制代码
// ========== 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 标识移动、新增、删除
jsx 复制代码
// ========== 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
并发支持
php 复制代码
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 的 useTransitionuseDeferredValue 依赖 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
jsx 复制代码
// ========== 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 依赖调用顺序形成链表)
jsx 复制代码
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
jsx 复制代码
// ========== 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 不匹配

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


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

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

能捕获 不能捕获
子组件渲染期间的错误 事件处理函数中的错误
生命周期方法中的错误 异步代码(setTimeout、Promise)
构造函数中的错误 服务端渲染的错误
错误边界自身的错误
jsx 复制代码
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>
}

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


9. React 中的 Portals 是什么?

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

jsx 复制代码
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: hiddenz-index 限制的场景。


10. React 中的 forwardRef 和 useImperativeHandle?

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

jsx 复制代码
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 模式 ✅ 直观 适用场景有限
jsx 复制代码
// ========== 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 服务端状态 缓存/重试/轮询
jsx 复制代码
// ========== 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 完整指南

路由配置方式

jsx 复制代码
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

jsx 复制代码
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>
  )
}

路由三种模式

模式 API URL 示例 特点
BrowserRouter createBrowserRouter /users/123 HTML5 History,需服务器配置
HashRouter createHashRouter /#/users/123 兼容旧浏览器,无需服务器配置
MemoryRouter createMemoryRouter 无 URL 变化 测试/非浏览器环境使用

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


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

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

jsx 复制代码
// ========== 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)

jsx 复制代码
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(数据请求利器)

jsx 复制代码
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+)
事件对象 原生 Event SyntheticEvent(跨浏览器包装)
事件池 --- React 16 有事件池(17 移除)
阻止冒泡 e.stopPropagation() 只阻止 React 事件冒泡
命名 onclick onClick(驼峰)
jsx 复制代码
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 + onChange ref.current.value
实时验证 ✅ 每次输入都可验证 ❌ 需要手动触发
性能 每次输入触发重渲染 ✅ 不触发重渲染
推荐场景 表单验证、联动 文件上传、简单表单
jsx 复制代码
// ========== 受控组件 ==========
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 函数组件)

kotlin 复制代码
类组件生命周期:
┌─────────────────────────────────────────────────┐
│ 挂载阶段                                         │
│  constructor → getDerivedStateFromProps →        │
│  render → componentDidMount                     │
├─────────────────────────────────────────────────┤
│ 更新阶段                                         │
│  getDerivedStateFromProps → shouldComponentUpdate│
│  → render → getSnapshotBeforeUpdate →           │
│  componentDidUpdate                              │
├─────────────────────────────────────────────────┤
│ 卸载阶段                                         │
│  componentWillUnmount                            │
├─────────────────────────────────────────────────┤
│ 错误处理                                         │
│  getDerivedStateFromError → componentDidCatch    │
└─────────────────────────────────────────────────┘
类组件生命周期 函数组件 Hook 对应
constructor useState 惰性初始化
getDerivedStateFromProps useState + 渲染期间更新
render 函数体本身
componentDidMount useEffect(() => {}, [])
componentDidUpdate useEffect(() => {}, [deps])
componentWillUnmount useEffect 返回的清理函数
shouldComponentUpdate React.memo
getSnapshotBeforeUpdate 无直接对应(用 useLayoutEffect)
componentDidCatch 无(仍需类组件)
jsx 复制代码
// ========== 函数组件等价实现 ==========
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 废弃了 componentWillMountcomponentWillReceivePropscomponentWillUpdate(加了 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
内联样式 --- 无伪类/媒体查询
jsx 复制代码
// ========== 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 Components RSC 正式稳定
jsx 复制代码
// ========== 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
并行/依赖查询 自动处理查询依赖关系
jsx 复制代码
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 增量静态生成 ✅✅ 快 数据定期更新的页面
RSC React Server Components ✅✅ 数据密集型页面
jsx 复制代码
// ========== 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。

jsx 复制代码
// ========== 经典闭包陷阱: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>
  )
}

解决方案大全

jsx 复制代码
// ========== 解决方案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 变化时重启定时器
}

常见闭包陷阱场景

jsx 复制代码
// ========== 场景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} />
}

闭包陷阱速查表

javascript 复制代码
// ========== 闭包陷阱场景和解决方案速查 ==========

| 场景                     | 问题                        | 解决方案                          |
|--------------------------|---------------------------|-----------------------------------|
| 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 合并为一次渲染,避免不必要的中间状态渲染。

jsx 复制代码
// ========== 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)

jsx 复制代码
// ========== 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:强制同步更新

jsx 复制代码
// ========== 跳出批量更新: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>
}

批量更新原理(简化)

jsx 复制代码
// ========== 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 APIReactDOM.render 不支持)。flushSync 是逃生舱口,用于需要立即同步 DOM 更新的场景(极少使用)。面试时还可以对比 Vue 的批量更新机制------Vue 从一开始就是全场景自动批量的(异步更新队列 + watcher 去重)。


25. React Suspense 的完整用法

Suspense 的核心概念

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

支持场景 React 版本 说明
React.lazy 代码分割 16.6+ 异步加载组件
Data Fetching 18+ 配合支持 Suspense 的库(TanStack Query、Relay)
Server Components 19+ RSC 中的流式渲染
use() Hook 19+ 直接在组件中读取 Promise
jsx 复制代码
// ========== 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

jsx 复制代码
// ========== 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 使用

jsx 复制代码
// ========== 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 原理(简化)

jsx 复制代码
// ========== 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 Components Client Components 传统 SSR
运行环境 仅服务器 服务器 + 客户端 服务器渲染,客户端 Hydrate
JS 发送到客户端 ❌ 不发送 ✅ 需要发送 ✅ 需要发送
访问后端资源 ✅ 直接访问 DB/文件 ❌ 需要 API ❌ 需要 API
useState/useEffect ❌ 不支持 ✅ 支持 ✅ 支持
事件处理(onClick) ❌ 不支持 ✅ 支持 ✅ 支持
包体积影响 ✅ 零影响 计入 Bundle 计入 Bundle
arduino 复制代码
// ========== 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"

jsx 复制代码
// ========== 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")

jsx 复制代码
// ========== 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 组件选型

arduino 复制代码
// ========== 组件选型速查表 ==========

| 需求                          | 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 接口。

相关推荐
禁止摆烂_才浅1 小时前
JavaScript 高级面试题
前端·javascript·面试
禁止摆烂_才浅1 小时前
JavaScript 基础 高频面试题
前端·javascript·面试
何时梦醒1 小时前
Docker 容器化入门:从「我电脑能跑」到「哪台机器都能跑」
后端·docker·面试
颜进强1 小时前
11 - 从需求拆解到 OpenSpec:为什么不要直接敲 /opsx:explore
前端·后端·ai编程
禁止摆烂_才浅1 小时前
HTML 高频面试题
前端·面试·html
一拳不是超人1 小时前
Godot 信号不是线程安全的:我是怎么在后台线程里翻车的
前端·架构
林太白1 小时前
What did Trea work help me with in this optimization process
前端·后端
一拳不是超人2 小时前
被 Tauri「体积小」种草后,我拿它做了个本地 AI 桌面工具,然后踩了这些坑
前端·架构
hyf3266332 小时前
高收益玩法:站群泛程序结合本地服务引流变现全流程
运维·服务器·前端·搜索引擎·蜘蛛池