Files
zhuangang/topics/frontend/react/01-hooks-rendering.md
T

101 lines
5.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: frontend.react.hooks-rendering
title: React 渲染机制、Hooks 规则、状态管理和 React 19 新特性
type: lesson
domain: frontend
stack: [react]
tags: [React, Hooks, useEffect, useReducer, 渲染, memo, 并发特性, React 19, React Compiler]
level: intermediate
prerequisites: [frontend.typescript.type-system]
est_hours: 3
related: [frontend.react.scenario.long-task-ui, frontend.ssr.nextjs-app-router, frontend.react.interview]
week: [7]
order: 1
---
# React 渲染机制、Hooks 规则、状态管理和 React 19 新特性
> 基于 React 19 编写,以官方文档为准:https://zh-hans.react.dev/ 。如果你主要用 Vue,这一课帮你建立 React 的心智模型,面试时能对比两者。
## 解释说明
### 渲染机制
- **组件每次渲染都会重新执行函数体**。状态变化(`setState`)、父组件渲染、Context 变化都会触发渲染。
- 渲染 → 生成新的元素树 → 和上一次比较(协调,reconciliation)→ 提交最小的 DOM 变更。列表的 `key` 决定元素的身份。
- 状态更新是**批量的**(React 18 起在 Promise、定时器中也自动批量)。
- 状态是**快照**:在一次渲染中读取的 state 是固定的,`setCount(count + 1)` 连续调用三次只加 1,要用函数式更新 `setCount(c => c + 1)`。
### Hooks 规则和常见坑
| Hook | 要点 |
| --- | --- |
| `useState` / `useReducer` | 复杂状态转换用 reducer,便于测试(见示例) |
| `useEffect` | 用于**与外部系统同步**(订阅、请求、DOM 操作),返回清理函数;依赖数组要完整;开发模式下严格模式会执行“挂载 → 卸载 → 挂载”帮助发现缺少清理的问题 |
| `useMemo` / `useCallback` / `memo` | 性能优化手段,不是语义保证;React Compiler 可以自动做记忆化(以官方文档为准) |
| `useRef` | 保存不触发渲染的可变值、DOM 引用 |
| 自定义 Hook | 复用有状态逻辑;必须以 `use` 开头,只在顶层调用 |
**规则**:只在组件或自定义 Hook 的顶层调用 Hook,不能放在条件、循环里——因为 React 按调用顺序识别每个 Hook 的状态。
**很多 `useEffect` 是不需要的**:从 props 计算出来的值直接在渲染中计算;用户事件引起的逻辑放在事件处理函数里。
### 状态管理选择
局部状态 `useState`;跨组件共享且变化不频繁用 Context;复杂的客户端状态用 Zustand / Redux Toolkit;**服务端数据**(请求、缓存、重试、失效)用 TanStack Query 等,而不是放进全局 store。
### React 19 和并发特性(了解)
- 并发特性:`useTransition`、`useDeferredValue` 把非紧急更新标记为可中断,保持输入流畅。
- React 19:Actions(异步函数作为 `action`,配合 `useActionState`、`useOptimistic` 处理表单提交、待定状态和乐观更新)、`use` 读取 Promise / Context、`ref` 可以作为普通 prop 传递。
- 服务端组件(Server Components)主要通过 Next.js 等框架使用,见 [Next.js 课程](../ssr/02-nextjs-app-router.md)。
## 案例
报表 Agent 的任务面板:任务有排队、运行、等待输入、成功、失败、取消等状态,服务端通过 SSE 推送事件,网络重连后可能收到重复或乱序的事件。用 `useReducer` 集中管理状态转换,事件带序号,旧事件和终态之后的事件直接丢弃;订阅在 `useEffect` 中建立并在任务切换 / 卸载时取消。
## 代码示例
reducer(完整版本 [examples/task-panel/taskReducer.ts](examples/task-panel/taskReducer.ts),`node taskReducer-demo.ts` 自检):
```ts
export function taskReducer(s: TaskState, e: TaskEvent): TaskState {
switch (e.type) {
case "submitted": return { ...initialState, id: e.id, status: "queued" };
case "cancel": return FINAL.has(s.status) ? s : { ...s, status: "cancelled" };
case "retry": return s.status === "failed" ? { ...s, status: "queued", error: null } : s;
default:
if (FINAL.has(s.status) || e.seq <= s.version) return s; // 丢弃重复、乱序、迟到的事件
}
...
}
```
组件([TaskPanel.tsx](examples/task-panel/TaskPanel.tsx),严格模式 `tsc` 检查通过):
```tsx
const [state, dispatch] = useReducer(taskReducer, initialState);
useEffect(() => {
dispatch({ type: "submitted", id: taskId });
const unsubscribe = subscribe(taskId, dispatch); // dispatch 引用稳定
return unsubscribe; // taskId 变化或卸载时取消订阅
}, [taskId, subscribe]);
```
## 易错点
- `useEffect` 依赖数组漏写,读到陈旧的值;或者为了“只执行一次”故意写空数组。
- 订阅、定时器、请求没有清理,切换页面后旧数据覆盖新数据(竞态)。
- 在渲染中直接修改 state 对象(`state.list.push(x)`),引用没变,不触发渲染。
- 用数组下标作为可重排列表的 `key`。
- 父组件每次渲染都创建新的对象 / 函数作为 props,导致 `memo` 子组件失效。
- 把服务端数据放进 Redux,自己处理缓存、失效、重试,代码复杂且容易出错。
## 面试常问
- React 组件什么时候会重新渲染?怎么避免不必要的渲染?
- 为什么 Hooks 不能写在条件语句里?
- `useEffect` 的执行时机和清理时机?
- React 和 Vue 的响应式 / 更新机制有什么区别?