> ## Documentation Index
> Fetch the complete documentation index at: https://opc-43d279b8.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# React 进阶

> 覆盖 Error Boundary、Context 性能、RSC/SSR、受控组件、keep-alive 与 Suspense 原理。

> 进阶这块我踩过的坑都在边界上：错误边界兜哪一层、Context 更新粒度多大、RSC 和 SSR 各自负责什么。API 会用不难，放错层就麻烦了。

***

## Q1: Error Boundaries 是什么？怎么实现？

### Error Boundaries 的捕获范围

Error Boundaries 能捕获：子组件树的渲染错误（render 方法中的同步错误）、生命周期方法中的错误、构造函数中的错误。不能捕获：事件处理函数中的错误（需要 try/catch）、异步代码中的错误（setTimeout/Promise）、服务端渲染的错误、Error Boundary 自身的错误。

### 生产环境的分层策略

大型应用通常在三个层级设置 Error Boundary：页面级（捕获页面级错误，展示全屏错误页）、功能模块级（捕获某个功能区域的错误，降级该区域）、组件级（捕获关键组件的错误，展示骨架屏）。配合 Sentry 等错误监控平台，实现错误上报和告警。

**1. 组件树的错误传播问题**

在 React 16 之前，组件树内部抛出的 JavaScript 错误会导致整个页面崩溃白屏。一个小组件的渲染错误，会让整个应用变得不可用，用户体验极差。

**2. Error Boundaries 的引入**

React 16 引入了错误边界机制，通过 `componentDidCatch` 生命周期和 `getDerivedStateFromError` 静态方法，允许开发者捕获子组件树中的渲染错误，展示一个降级的备用 UI。

**3. 错误边界的局限性**

Error Boundaries 只能捕获子组件树的渲染错误，无法捕获事件处理函数、异步代码（setTimeout/Promise）、服务端渲染和 Error Boundary 自身的错误。

**4. 函数组件的替代方案**

目前 React 没有内置的函数组件 Error Boundary。社区通过 `react-error-boundary` 库提供了函数式 API，支持 `fallback` 属性和 `onReset` 回调。

**5. 生产环境的最佳实践**

在生产环境中，通常会在页面级和功能模块级分别设置 Error Boundary，配合错误监控平台（如 Sentry）实现优雅降级和错误上报。

**Error Boundaries 解决的核心问题：**

```text theme={null}
问题：一个组件的渲染错误导致整个页面崩溃

用户操作 → 某个子组件抛出错误 → 整个组件树卸载 → 白屏

解决：Error Boundary 捕获错误，只卸载出错的子树，其余部分正常运行

用户操作 → 某个子组件抛出错误 → Error Boundary 捕获 → 显示降级 UI → 其余组件正常
```

> [!NOTE]
>
> Error Boundaries 让 React 应用从"一错全崩"变成了"局部降级"。

> [card] 没有 Error Boundaries
>
> 一个小组件的渲染错误导致整个页面白屏。就像一栋大楼里一间房着火了，整栋楼的所有住户都被强制撤离。

> [card] 引入 Error Boundaries
>
> 出错的房间被隔离，显示"维修中"的提示，其他住户正常生活。就像大楼装了防火门，火势不会蔓延。

> [!TIP]
>
> 回答时先说"为什么需要"（组件错误导致全页面崩溃），再说"怎么实现"（getDerivedStateFromError + componentDidCatch）。最后一定要主动说出它的局限性（不能捕获异步错误），这是加分项。

***

## Q2: Context 的性能问题怎么解决？

Context 是 React 内置的跨组件数据传递机制，避免了层层传递 Props（Prop Drilling）的问题。但它的更新机制是粗粒度的——只要 Provider 的 value 发生变化，所有消费该 Context 的组件都会强制 re-render。

### Context 的弊端总结

Context 的核心问题有三个：第一，粗粒度更新——Provider 的 value 变化时，所有 Consumer 强制 re-render，无法按需订阅；第二，绕过 React.memo——即使子组件被 `React.memo` 包裹，只要它通过 `useContext` 消费了变化的 Context，就会强制 re-render；第三，组合困难——多个 Context 嵌套时，组件需要层层嵌套 Provider，代码可读性差。

### 状态管理方案技术选型

| 方案 | 原理 | 适用场景 | 学习成本 |
| - | - | - | - |
| **Context** | Provider/Consumer 广播 | 低频变化的全局数据（主题、语言） | 低 |
| **Redux** | 单一 Store + 纯函数 Reducer | 复杂业务逻辑、需要时间旅行调试 | 高 |
| **Zustand** | 原子化 Store + 自动订阅 | 中小型应用、精准订阅 | 低 |
| **MobX** | 响应式数据 + 自动追踪依赖 | 复杂对象图、需要细粒度响应 | 中 |
| **Jotai** | 原子化状态 + 底层 Context | 独立的、细粒度的状态单元 | 低 |

```text theme={null}
┌───────────────────────────────────────┐
│         ThemeProvider (Context)        │  ← value 变了
└─────────────────┬─────────────────────┘
                  │
      ┌───────────┼───────────┐
      ▼           ▼           ▼
  ┌──────┐   ┌──────┐   ┌──────┐
  │Comp A│   │Comp B│   │Comp C│   ← 全部 re-render，即使只用了部分数据
  └──────┘   └──────┘   └──────┘
```

```javascript theme={null}
// 问题：Context 值变化时，所有 Consumer 都会 re-render
const ThemeContext = createContext({ theme: "light", lang: "zh" });

// CompA 只用了 theme，CompB 只用了 lang
// 但 theme 变化时，CompB 也会 re-render（即使 lang 没变）

// 解决方案 1：拆分 Context
const ThemeContext = createContext("light");
const LangContext = createContext("zh");
// theme 变化时，只有 ThemeContext 的 Consumer re-render

// 解决方案 2：用 useMemo 稳定 Context 值
function App() {
  const [theme, setTheme] = useState("light");
  const value = useMemo(() => ({ theme, setTheme }), [theme]);
  return <ThemeContext.Provider value={value}>...</ThemeContext.Provider>;
}

// 解决方案 3：使用原子化状态库（Jotai、Zustand）
// 精准订阅，只有用到的组件 re-render
```

> [!WARNING]
>
> Context 的 value 变化时，会绕过 `React.memo` 的优化。即使子组件被 `React.memo` 包裹，只要它通过 `useContext` 消费了变化的 Context，就会强制 re-render。这是很多开发者在优化列表性能时忽略的"隐性更新源"。

> [!TIP]
>
> Context 就像公司的全员邮件。你改了公司 logo（theme 变了），系统给所有员工（Consumer）都发了一封通知邮件，即使有些员工根本不关心 logo。拆分 Context 就像把邮件按部门分组——设计部收到 logo 变更通知，财务部只收到报销规则变更通知，互不干扰。

***

## Q3: React Server Components 和 SSR 有什么区别？

### SSR 的 Hydration 成本

SSR 的根本问题是 Hydration 成本。服务端渲染出 HTML 后，客户端必须下载完整的组件代码，在本地重新执行一遍来"激活"交互（Hydration）。这意味着组件代码被执行了两次——服务端一次生成 HTML，客户端一次绑定事件。对于大型应用，Hydration 可能需要数百毫秒，阻塞了页面的可交互时间（TTI）。

### RSC 的流式传输协议

RSC 使用一种特殊的序列化格式（RSC Payload）流式传输渲染结果。协议中包含模块引用（Module References），告诉客户端需要下载哪些客户端组件的代码。服务端组件的代码完全不发送到客户端，从根本上减少了客户端 JS 体积。

### 基础梳理

| 维度 | SSR | Server Components |
| - | - | - |
| **渲染产物** | HTML 字符串 | RSC Payload（序列化格式） |
| **客户端需要的 JS** | 需要完整组件代码进行 Hydration | 服务端组件的代码不发送到客户端 |
| **运行时机** | 请求时在服务端渲染 HTML | 组件在服务端运行，结果流式传输 |
| **交互能力** | Hydration 后完全可交互 | 服务端组件不可交互，客户端组件可交互 |
| **数据获取** | 在 getServerSideProps 中获取 | 直接在组件中 async/await |

### 专家视角深度剖析

很多人只能说出"RSC 减少了客户端 JS 体积"，但实际更在意的是：**RSC 的核心设计理念是什么？它解决了 SSR 的什么根本性问题？**

SSR 的根本问题是**Hydration 成本**。服务端渲染出 HTML 后，客户端必须下载完整的组件代码，在本地重新执行一遍来"激活"交互（Hydration）。这意味着组件代码会被执行两次——服务端一次生成 HTML，客户端一次绑定事件。对于大型应用，Hydration 可能需要数百毫秒，阻塞了页面的可交互时间。

RSC 的设计理念是**组件级别的服务端/客户端分离**。服务端组件只在服务端运行，客户端收到的是渲染结果（RSC Payload），不需要重新执行组件函数。只有标记了 `'use client'` 的客户端组件才需要发送 JS 到客户端并进行 Hydration。

```jsx theme={null}
// 服务端组件：只在服务端运行，不发送 JS 到客户端
async function UserProfile({ userId }) {
  const user = await db.query("SELECT * FROM users WHERE id = ?", [userId]);
  return <div>{user.name}</div>;
  // 客户端只收到渲染结果，不需要这个组件的代码
}

// 关键规则：服务端组件可以导入客户端组件，但客户端组件不能导入服务端组件
// 客户端组件可以接收服务端组件作为 children prop
```

```jsx theme={null}
// 客户端组件（另一个文件）：发送到客户端，需要 Hydration
"use client"; // 必须是该文件的第一条语句，且在 import 之前

import { useState } from "react";

function LikeButton() {
  const [liked, setLiked] = useState(false);
  return (
    <button onClick={() => setLiked(!liked)}>{liked ? "Liked" : "Like"}</button>
  );
}
```

***

## Q4: 受控组件和非受控组件的区别是什么？

**受控组件的表单值由 React state 管理，非受控组件的表单值由 DOM 自身管理。** 这是两者最根本的区别。

### 受控组件的实现原理

受控组件的核心是"单一数据源"——React state 是表单值的唯一真实来源。每次用户输入都触发 `onChange` 事件，事件处理器调用 `setState` 更新 state，React 重新渲染并将新的 `value` 传回 input。这形成了一个单向数据流：用户输入 → onChange → setState → re-render → input.value 更新。

### 非受控组件的适用场景

非受控组件适合以下场景：文件上传（`<input type="file">` 的值只能由用户设置，不能由程序设置）、集成非 React 第三方库（如地图、富文本编辑器）、性能敏感场景（避免每次输入都触发 re-render）。使用 `ref` 直接访问 DOM 节点获取值，或者用 `defaultValue` 设置初始值。

```javascript theme={null}
// 受控组件：React 是"单一数据源"
function ControlledInput() {
  const [value, setValue] = useState("");
  return (
    <input
      value={value} // React 控制值
      onChange={(e) => setValue(e.target.value)} // 每次输入都更新 state
    />
  );
}
// 优势：实时验证、动态禁用、格式化输入

// 非受控组件：DOM 自己管理值
function UncontrolledInput() {
  const inputRef = useRef();
  const handleSubmit = () => {
    console.log(inputRef.current.value); // 提交时才从 DOM 读取值
  };
  return <input ref={inputRef} defaultValue="初始值" />;
}
// 优势：集成非 React 代码、文件上传、性能敏感场景
```

**关键区别：**

| 场景 | 受控组件 | 非受控组件 |
| - | - | - |
| 值的来源 | React state | DOM 自身 |
| 获取值的方式 | 读 state | 读 ref.current.value |
| 适用场景 | 实时验证、格式化、动态禁用 | 文件上传、集成第三方库 |
| 性能 | 每次输入触发 re-render | 不触发 re-render |

> [!TIP]
>
> 默认使用受控组件（符合 React "单一数据源"的理念）。只在文件上传、集成非 React 第三方库等场景下使用非受控组件。

***

## Q5: React 为什么没有 keep-alive？

### React 官方的设计决策

Vue 的 `<keep-alive>` 是一个内置组件，用于缓存不活跃的组件实例，切换时保留组件状态。React 从设计之初就没有提供等价的内置机制，这不是疏忽，而是有意为之。

### 架构设计上的考量

React 的核心设计理念是**声明式 UI + 单向数据流**。组件的输出完全由 props 和 state 决定，组件卸载后状态消失是符合预期的行为——因为状态应该由父组件或状态管理库（Redux、Zustand）来"持有"，而不是由组件实例自身持有。

```text theme={null}
Vue 的 keep-alive 思路：
  组件实例自带状态 → 切走时缓存实例 → 切回时恢复实例 + 状态

React 的设计思路：
  状态属于数据层（Redux/Zustand） → 组件只是状态的"视图" → 卸载重建无所谓
```

### Fiber 架构与并发模式的影响

React 18 的并发模式（Concurrent Mode）进一步强化了"组件可随时卸载"的假设。在并发渲染中，React 可能会**预渲染**尚未展示的组件，也可能**丢弃**已经渲染但不再需要的组件。如果引入 keep-alive 这种"实例持久化"机制，会与并发模式的调度策略产生冲突。

```text theme={null}
并发模式的调度行为：

React：我预渲染了 TabB 的内容，但用户没点过去，我决定丢弃它
  → 如果有 keep-alive，这个"丢弃"就变成了"缓存"，语义冲突

React：用户切换到 TabB，但优先级不够高，我先中断渲染去做更重要的事
  → 如果有 keep-alive，缓存的实例可能和当前 props 不同步
```

### React 为什么不提供 keep-alive？

三个核心原因：

| 原因 | 解释 |
| - | - |
| **状态应由数据层持有** | React 认为组件状态应该提升到 Redux/Zustand/URL，而不是由组件实例"私有" |
| **并发模式兼容** | keep-alive 的"实例持久化"与并发模式的"可丢弃渲染"冲突 |
| **组合优于缓存** | React 推荐用条件渲染 + 状态提升来替代缓存，保持组件树的简洁 |

> [!NOTE]
>
> React 没有 keep-alive 不是功能缺失，而是设计哲学的选择——**状态属于数据层，组件只是视图函数，卸载重建是正常行为**。

***

## Q6: React 中如何实现 keep-alive 效果？

虽然 React 没有内置 keep-alive，但有多种方式实现类似效果。

### 方案一：状态提升（Lifting State Up）

最符合 React 理念的方式——把组件的状态提升到父组件，子组件卸载重建时状态不丢失。

```jsx theme={null}
function TabContainer() {
  // 状态在父组件，子组件卸载不影响
  const [tabAData, setTabAData] = useState([]);
  const [tabBData, setTabBData] = useState([]);
  const [activeTab, setActiveTab] = useState("A");

  return (
    <>
      <TabBar active={activeTab} onChange={setActiveTab} />
      {activeTab === "A" && <TabA data={tabAData} onChange={setTabAData} />}
      {activeTab === "B" && <TabB data={tabBData} onChange={setTabBData} />}
    </>
  );
  // Tab 切换时组件会卸载重建，但数据不丢失
}
```

**局限性**：只能保存 React state，无法保存 DOM 状态（滚动位置、焦点、输入法状态）。

### 方案二：useRef 缓存组件实例

用 `useRef` 在父组件中缓存子组件的 VNode，切换时不销毁。

```jsx theme={null}
import { useRef, useState } from "react";

function KeepAlive({ children, activeKey }) {
  const cacheRef = useRef(new Map());

  // 缓存当前活跃的组件
  if (!cacheRef.current.has(activeKey)) {
    cacheRef.current.set(activeKey, children);
  }

  // 渲染所有缓存的组件，用 display 控制显隐
  return (
    <>
      {Array.from(cacheRef.current.entries()).map(([key, vnode]) => (
        <div
          key={key}
          style={{ display: key === activeKey ? "block" : "none" }}
        >
          {vnode}
        </div>
      ))}
    </>
  );
}

// 使用
function App() {
  const [activeTab, setActiveTab] = useState("A");
  return (
    <KeepAlive activeKey={activeTab}>
      {activeTab === "A" ? <TabA /> : <TabB />}
    </KeepAlive>
  );
}
```

**局限性**：所有缓存的组件都在 DOM 中，内存占用高；`display: none` 的组件仍然消耗渲染资源。

### 方案三：第三方库

```bash theme={null}
# react-activation：最接近 Vue keep-alive 的实现
npm install react-activation

# 使用
import { AliveScope, KeepAlive } from 'react-activation';

function App() {
  return (
    <AliveScope>
      <KeepAlive name="TabA">
        <TabA />
      </KeepAlive>
      <KeepAlive name="TabB">
        <TabB />
      </KeepAlive>
    </AliveScope>
  );
}
```

react-activation 的思路是在 `AliveScope` 下维护一份隐藏的缓存容器，把需要保留的组件树渲染进去；切换时复用缓存中的节点，而不是重新挂载。该库依赖 React 内部机制，升级 React 大版本时需要重点验证兼容性。

### 方案四：手写 keep-alive 原理

核心思路：用一个 Map 缓存组件的 VNode，用 DOM 的 `appendChild` 把组件的真实 DOM 移到容器中，而不是销毁重建。

```jsx theme={null}
import {
  useRef,
  useEffect,
  useCallback,
  createContext,
  useContext,
} from "react";

const KeepAliveContext = createContext();

// 提供缓存容器
function AliveScope({ children }) {
  const cacheRef = useRef(new Map());

  const keep = useCallback((id, dom) => {
    cacheRef.current.set(id, dom);
  }, []);

  const release = useCallback((id) => {
    cacheRef.current.delete(id);
  }, []);

  return (
    <KeepAliveContext.Provider value={{ keep, release, cache: cacheRef }}>
      {children}
      <div id="alive-container" style={{ display: "none" }}>
        {/* 缓存的 DOM 节点存放在这里 */}
      </div>
    </KeepAliveContext.Provider>
  );
}

// 缓存组件的 DOM
function KeepAlive({ id, children }) {
  const containerRef = useRef();
  const { keep, release, cache } = useContext(KeepAliveContext);

  useEffect(() => {
    const container = document.getElementById("alive-container");
    const cachedDom = cache.current.get(id);

    if (cachedDom) {
      // 从缓存中恢复 DOM
      containerRef.current.appendChild(cachedDom);
    }

    return () => {
      // 卸载时把 DOM 移到缓存容器，而不是销毁
      if (containerRef.current?.firstChild) {
        container.appendChild(containerRef.current.firstChild);
      }
    };
  }, [id]);

  return <div ref={containerRef}>{children}</div>;
}
```

> [!WARNING]
>
> 手写的 keep-alive 只能缓存 DOM 节点，无法缓存 React 组件的内部状态（hooks state、ref 等）。如果需要完整的状态缓存，应该使用 react-activation 等第三方库。状态提升方案虽然简单，但最符合 React 的设计理念。

### 方案选型对比

| 方案 | 缓存 DOM | 缓存状态 | 性能开销 | 复杂度 |
| - | - | - | - | - |
| **状态提升** | 否 | 是（state） | 低 | 低 |
| **useRef + display** | 是 | 是 | 中（隐藏组件仍渲染） | 中 |
| **react-activation** | 是 | 是 | 中 | 低 |
| **手写 DOM 转移** | 是 | 否（hooks 丢失） | 低 | 高 |

> [!NOTE]
>
> React 没有 keep-alive，但**状态提升是最 React 的方案**——把状态放到数据层，组件只是视图，卸载重建无所谓。如果确实需要缓存 DOM 状态（滚动位置），用 react-activation。

***

> **Redux / Zustand / MobX 的原理详解和对比选型**，请参见 [状态管理](https://opc-43d279b8.mintlify.site/state-management/state-management.md) 章节。该模块覆盖了 Redux 三大原则、Zustand 极简实现、MobX 响应式原理、以及三者的技术选型对比。

***

## Q7: Suspense 在源码中是怎么工作的？

Suspense 在源码中是怎么实现的？

当子组件"抛出"一个 Promise（通过 `throw`，不是 `return`），React 的 Fiber 协调器会捕获它并渲染最近的 Suspense boundary 的 fallback。Promise resolve 后，React 重新尝试渲染子组件。详见 [React 源码 packages/react-reconciler/src/ReactFiberWorkLoop.js](https://github.com/facebook/react/blob/main/packages/react-reconciler/src/ReactFiberWorkLoop.js)。

```javascript theme={null}
// Suspense 的核心机制（伪代码，便于理解流程，函数名并非源码中的真实 API）
function renderRoot(root) {
  try {
    // 尝试渲染组件树
    workLoopSync();
  } catch (thrownValue) {
    // 如果组件 throw 了一个 Promise（thenable）
    if (isThenable(thrownValue)) {
      // 在 Fiber 树上向上查找最近的 Suspense boundary
      const suspenseBoundary = findNearestSuspenseBoundary();
      // 将该 boundary 标记为挂起，本次提交渲染 fallback
      markSuspenseBoundarySuspended(suspenseBoundary);
      // 等待 Promise resolve 后重新调度该 boundary 的渲染
      thrownValue.then(() => {
        scheduleUpdateOnFiber(suspenseBoundary);
      });
    }
  }
}
```

这段代码做了 2 件事：第一，通过 try/catch 捕获组件 throw 的 Promise，找到最近的 Suspense boundary；第二，将 boundary 状态标记为 Suspended 并渲染 fallback，Promise resolve 后触发重新渲染。

React 19 的 use() hook 是怎么配合 Suspense 的？

`use()` hook 是 React 19 引入的新 API，它可以在渲染过程中"读取" Promise 或 Context。当 Promise 未 resolve 时，`use()` 会 throw 一个 Promise，触发最近的 Suspense boundary 渲染 fallback。与 `useEffect` + `useState` 的数据获取方式不同，`use()` 可以在条件语句和循环中使用，打破了 Hooks 的"顶层调用"规则。详见 [React 官方文档 use()](https://react.dev/reference/react/use)。

***

## Q8: 及格回答和满分回答差在哪？

### 问题："说说 React Server Components"

**及格回答（60-70分）：**

> "RSC 在服务端渲染组件，不发送 JS 到客户端。减少客户端 JS 体积。"

问题出在：**知道结论但不理解设计**。

**满分回答（90-100分）：**

> RSC 的核心思想是**组件级别的服务端/客户端分离**。SSR 是把整个页面在服务端渲染为 HTML 字符串，客户端下载完整组件代码后进行 Hydration（重新执行一遍绑定事件）。这意味着组件代码被执行了两次。RSC 不同——服务端组件只在服务端运行，客户端收到的是 RSC Payload（一种特殊的序列化格式），不需要重新执行组件函数。

> RSC 的关键设计约束是：服务端组件不能用 useState/useEffect 等 Hooks，不能用浏览器 API。但它可以直接访问数据库、文件系统，不需要 API 层。客户端组件通过 `'use client'` 指令标记，需要发送到客户端并进行 Hydration。服务端组件可以导入客户端组件，但客户端组件不能导入服务端组件（因为客户端无法执行服务端代码）。客户端组件可以接收服务端组件作为 children prop。

> RSC 解决了 SSR 的根本问题——Hydration 成本。对于大型应用，Hydration 可能阻塞数百毫秒的可交互时间。RSC 通过减少需要发送到客户端的 JS 代码量（服务端组件的代码完全不发送），从根本上降低了 Hydration 的工作量。

### 常见追问

> [accordion] Error Boundary 能捕获异步错误吗?
>
> **答题要点：**
> 明确 Error Boundary 的捕获范围。
>
> **高分回答：**
> "不能。Error Boundary 只能捕获子组件树在**渲染过程**中的同步错误。异步错误（如 setTimeout、Promise 中的错误）、事件处理函数中的错误、服务端渲染的错误、Error Boundary 自身的错误都无法捕获。异步错误需要在回调中用 try/catch 或 .catch() 处理，然后通过 setState 更新状态触发重新渲染。事件处理函数中的错误需要用普通的 try/catch 捕获。"

> [accordion] Context 的 value 变化时, 为什么 React.memo 会被绕过?
>
> **答题要点：**
> 从 React 的更新传播机制解释。
>
> **高分回答：**
> "React 的更新传播有两条路径：一条是通过 Props 从父组件向下传递（受 React.memo 优化），另一条是通过 Context 的 Provider 直接通知所有 Consumer。当 Context 的 value 变化时，React 会强制标记所有消费该 Context 的 Fiber 节点需要重新渲染，这个标记发生在 `readContext` 函数中，它会将 Consumer 注册到 Context 的订阅列表。由于这个标记是在 Fiber 的 `lanes` 层面设置的，`React.memo` 的 Props 浅比较根本不会被触发——更新已经在更底层被标记了。"

> [accordion] Hydration 失败会怎样?
>
> **答题要点：**
> 从用户体验和性能两个角度分析后果。
>
> **高分回答：**
> "当服务端和客户端的渲染结果不一致时，React 会报 hydration mismatch 警告，并回退到客户端渲染来修正不一致的子树（严重时表现为整棵树的客户端渲染）。这会导致：第一，用户可能看到页面闪烁；第二，丢失部分服务端渲染带来的首屏收益。常见原因包括：使用了 Date.now()、Math.random()（服务端和客户端结果不同）、渲染时直接读取 window/document（服务端不存在）。解决方案是让首屏渲染结果保持确定性，把客户端特有逻辑放进 `useEffect`，必要时用 `suppressHydrationWarning` 处理不可避免的文本差异。"

> [accordion] React 19 的 use() hook 打破了 Hooks 规则, 为什么可以这样做?
>
> **答题要点：**
> 从实现机制和设计动机两个角度分析。
>
> **高分回答：**
> "传统的 Hooks（如 useState）必须在函数顶层调用，因为它们依赖链表的顺序读取机制——React 按调用顺序从 memoizedState 链表中取值。但 `use()` 的实现不依赖链表顺序：它不是从 Fiber 节点上读取缓存状态，而是直接在渲染过程中同步读取 Promise 或 Context 的当前值。如果 Promise 未 resolve，它 throw 一个 Promise 触发 Suspense；如果已 resolve，它返回值。这种实现方式不依赖调用顺序，所以可以在条件语句和循环中使用。设计动机是简化数据获取——不再需要 useEffect + useState + loading 三件套。注意传给 `use()` 的 Promise 必须来自上层或缓存（不要在渲染中每次新建 Promise），否则每次渲染都会重新触发挂起。"

> [accordion] 受控组件和非受控组件该怎么选?
>
> **答题要点：**
> 从 React 设计理念和实际场景两个角度分析。
>
> **高分回答：**
> "默认使用受控组件——它符合 React '单一数据源'的理念，React state 是表单值的唯一真实来源，便于实时验证、格式化输入、动态禁用。非受控组件适用于三种场景：文件上传（`<input type='file'>` 的值只能由用户设置，不能由程序设置）、集成非 React 第三方库（如地图、富文本编辑器）、性能敏感场景（避免每次输入都触发 re-render）。非受控组件用 `ref` 直接访问 DOM 节点获取值，用 `defaultValue` 设置初始值。注意不要混用——同一个表单中，部分字段受控、部分非受控，会导致状态管理混乱。"

***

## 延伸阅读与引用参考

* [React 官方文档：Error Boundaries](https://react.dev/reference/react/Component#catching-rendering-errors-with-an-error-boundary)：官方对错误边界的权威解释和使用示例。<sup>\[1]</sup>
* [React 官方文档：Server Components](https://react.dev/reference/rsc/server-components)：官方对 RSC 设计理念和使用方式的完整文档。<sup>\[2]</sup>
* [Dan Abramov: Suspense for Data Fetching](https://react.dev/blog/2022/03/29/react-v18.html)：深入理解 Suspense 的工作原理和未来方向。<sup>\[3]</sup>
* [react-error-boundary 库](https://github.com/bvaughn/react-error-boundary)：函数组件的 Error Boundary 实现，支持 fallback 和 onReset。<sup>\[4]</sup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.