组件与 JSX:函数组件、props、state 与组合
0. 元信息
- 主题路径:
docs/topics/react-app/subtopics/components-and-jsx/README.md - 父主题:
react-app - 主分类:前端与客户端
- 辅助分类:工程技术
- 适合对象:会用 HTML/CSS/JS 但没系统写过 React 的开发者
- 建议周期:1~1.5 周
- 前置知识:JavaScript ES Module、解构、箭头函数、模板字符串
- 最终目标:用纯函数组件 + Hooks 写出一个可读、可测、可演化的应用骨架
1. 学习路线
JSX 语法与编译 → 函数组件 → props / children / 组合 → state 与受控输入
→ 列表与 key → 状态提升 → useRef / Portal / 错误边界 → 表单与受控/非受控
2. 阶段周数分配
精简子主题不固定周数。按 §3 顺序完成,每段都能跑一个最小 demo。
3. 九阶段表
| 阶段 | 核心知识 | 实践产出 | 学会标准 |
|---|---|---|---|
| 1 | JSX 规则、表达式、属性、条件渲染、列表 | 卡片 / 头像 demo | 能区分“表达式”和“语句” |
| 2 | 函数组件、props 默认值、TypeScript JSX 类型 | Button / Card / Avatar | 能写 prop 类型与 children 类型 |
| 3 | 组合 vs 继承、slot 模式、render props 简介 | Tabs / Dialog | 能用 children 做插槽 |
| 4 | useState、setState 是异步批、对象/数组不可变更新 | 计数器、Todo 列表 | 能解释“闭包里读旧 state”的成因 |
| 5 | 受控 vs 非受控、useRef、defaultValue | 搜索框、文件上传 | 不混用 value + defaultValue |
| 6 | 列表 + 稳定 key、key 的语义 | 1000 项 todo | 不再用 index 当 key |
| 7 | 状态提升、Context 引入 | 选中态 / 主题 | 能决定状态该放哪 |
| 8 | createPortal、错误边界、lazy + Suspense | Modal、ErrorBoundary | 知道错误“冒泡到边界” |
| 9 | 表单库选型:react-hook-form / Formik / 原生 | 注册表单 | 能选最小的库 |
4. 第一周任务
精简版省略固定日程。按 §3 顺序做最小可运行 demo,再补边界用例。
5. 阶段通用验收
精简版省略;每个产出至少保留命令、版本、实际输出。
6. 最终验收
精简版省略;以 §3 第 9 阶段和 §9.4 案例口述检查为准。
7. 综合项目
精简版省略;成果并入父主题电商 SPA 的“基础组件库 + 路由壳”。
本主题贡献
- 职责 1(受控 / 非受控表单与状态归属):负责把父主题里“商品列表、购物车、登录表单、Modal 详情”四类交互明确划分为“表单走
value+onChange、高频输入(如搜索)走useDeferredValue、瞬时态走useRef”,统一约定useStatesetState 异步批,禁止把useState当同步读,杜绝 §9.4 案例 #3、#4 出现的“读旧 state / 受控值不变”。 - 职责 2(稳定 key 与列表身份语义):所有
Array.map渲染列表强制使用业务id做 key(参考 §3 阶段 6、§9.4 #1),并对“1000 项 todo 增删改后状态错位”做端到端复现——删除中间项后,输入框焦点与勾选态必须留在原条目;只有当数据源真没有id(如纯前端 mock)才允许index,但必须在文件头注释声明 TODO。 - 职责 3(错误边界 + Portal + 焦点归还):在父主题“商品详情 Modal / 全局 ErrorBoundary”节点落地
createPortal(modalNode, document.body)+ classErrorBoundary,并在关闭 modal 时调用ref.current.focus()回到触发按钮(§9.4 #5);事件处理器内的异常由业务自行try/catch上报,不依赖 ErrorBoundary 捕获。 - 交付物 1:基础组件库
packages/ui内含 ≥8 个原子组件(Button / Card / Avatar / Tabs / Dialog / Modal / ErrorBoundary / FormField),每个组件都带*.stories.tsx与*.test.tsx。 - 交付物 2:
examples/todo-1000复现“index做 key 导致状态错位 vsid做 key 状态稳定”的对比 demo,浏览器可视对照;脚本可一键pnpm dev启动并切换两种 key 模式。 - 交付物 3:
examples/modal-portal提供“按 Esc 关闭、关闭后焦点回到打开按钮、点击遮罩关闭”三件套;用 React DevTools Profiler 录一段交互 trace 作为证据。 - 交付物 4:表单组件文档明确给出“受控 vs 非受控”决策表(搜索/筛选 → 受控;文件上传/富文本 → 非受控 + ref),并在
FormFieldAPI 注释里固化这一约定。 - 指标 1:基础组件库 ≥8 个原子组件 + ≥3 个复合组件(Tabs / Dialog / Modal)。
- 指标 2:仓库全局 grep
key={index}命中数为 0;唯一允许的indexkey 必须在文件头声明 TODO 注释。 - 指标 3:Modal/对话框类组件 100% 具备“关闭后焦点归还触发元素”能力,由 RTL 测试断言(
expect(document.activeElement).toBe(triggerBtn))。
8. 推荐资料
精简版省略;使用 §9.3 和 §9.6 Source。
9. 学习资料汇聚(v0.3 自包含)
9.1 背景与动机
React 把 UI 抽象为“状态到视图”的纯函数。组件是边界、props 是输入、JSX 是声明式语法糖。理解组件模型是理解 React 一切后续概念(Hooks、Suspense、RSC)的基础。错误往往不是“不会 Hook”,而是“状态放错位置、props 透传太多、key 用错”。
9.2 概念地图
flowchart LR
JSX[JSX] --> Comp[函数组件]
Comp --> Props[props / children]
Comp --> State[useState]
State --> Lifted[状态提升]
Lifted --> Context
Comp --> Ref[useRef]
Comp --> Portal[createPortal]
Comp --> Error[ErrorBoundary]
Comp --> Lazy[lazy + Suspense]
State --> Form[受控/非受控]
Form --> Lib[react-hook-form / Formik]
9.3 基础知识讲解
9.3.1 论文 / 规范
- React 官方文档:Thinking in React——按层级拆组件、找最小 state、放对位置。
- ECMAScript 提案历史中的 JSX(由 TypeScript / Babel / SWC 实现)。
- React 19 Release Notes:
ref as prop不再需要forwardRef。
9.3.2 书
- Learning React(Alex Banks / Eve Porcello)。
- React Docs Beta → react.dev。
- Epic React(Kent C. Dodds)——基础模块。
9.3.3 博客 / 文档
- react.dev:Your First Component。
- react.dev:Thinking in React。
- Overreacted: Why Do React Elements Have Typeof Symbol?。
- Josh Comeau:Common Beginner Mistakes。
9.3.4 人物
- Dan Abramov:Hooks 倡导者、官方文档主笔。
- Kent C. Dodds:Testing Library、Epic React、a11y 实践。
- Brian Vaughn:React core、key 的真实作用。
- Sophie Alpert:React core 早期 hook 推广者。
9.3.5 方法
- Composition over inheritance:UI 复用靠组合,不是类继承。
- Lift state up:状态放最近共同祖先;跨页面再走 store。
- Single source of truth:每个状态只在一处定义。
- Stable identity for key:key 是身份,不是位置。
- Controlled for sync, uncontrolled for perf:表单能受控就受控;超大表单 / 文件用 ref。
9.4 经典问题与经典案例
| # | 问题 | 为什么重要 | 最简答案或证据 |
|---|---|---|---|
| 1 | 用 index 做 key | 重排 / 过滤后状态错位 | 用稳定 id |
| 2 | 受控组件值不变 | onChange 没更新 state | 走 onChange={e => setX(e.target.value)} |
| 3 | setState 立刻读不到 | 批处理异步 | 用 useEffect / useReducer;或接受旧值 |
| 4 | 闭包陷阱(stale closure) | effect 拿到旧 state | 加依赖或用 ref |
| 5 | Portal 焦点丢失 | modal 关闭后焦点跑走 | 关闭时 ref.current.focus() 回到触发元素 |
| 6 | ErrorBoundary 不生效 | 事件处理器不抛到边界 | 事件内 try/catch;或在边界内调用 |
| 7 | 受控 + defaultValue 混用 | React 警告 | 选其一 |
| 8 | props 透传(prop drilling) | 跨 3 层以上 | 用 Context / 组合 / store |
| 9 | 列表超大卡顿 | DOM 太多 | 虚拟列表 / 分页 / windowing |
| 10 | children 类型丢失 | TS 报“找不到属性” | 用 React.ReactNode 或自定义 prop |
9.5 学习难点
- 概念难点:JSX 不是 HTML;
class/for是className/htmlFor;表达式 vs 语句;React.Fragment简化。 - 思维难点:状态放哪。规则:URL 公开 / 跨页用 store / 同页父提 / 服务端数据用 RQ。
- 工程难点:组件边界、prop 接口设计、a11y 焦点管理、错误边界位置。
9.6 技术标准与接口
9.6.1 Entity
| 名称 | 版本 | 组织 | 状态 |
|---|---|---|---|
| React | 19 | Meta | 活跃 |
| React DOM | 19 | Meta | 活跃 |
| TypeScript JSX(TSX) | TS 5+ | Microsoft | 活跃 |
| react-hook-form | 7+ | 社区 | 活跃 |
| Formik | 2+ | 社区 | 维护中 |
9.6.2 Scope
- React 自身不规定 props 校验,由 TypeScript / PropTypes / Zod 负责。
- 表单库不替代原生
<form>,只减少样板。 - ErrorBoundary 是 React 16 引入的约定,class 组件仍保留。
9.6.3 Structure
- 必会 API:
useState/useRef/useEffect/useId/lazy/Suspense/createPortal。 - 必会 prop:
children、className、style、ref、key。 - 必会事件:
onClick/onChange/onSubmit/onFocus/onBlur。
9.6.4 Ecosystem
- 状态:Zustand、Jotai、RTK。
- 表单:react-hook-form、Formik、TanStack Form、Conform(与 RSC 配合)。
- 校验:Zod、Valibot、Yup。
- UI 库:shadcn/ui、Radix、Headless UI、Material UI、Chakra、Mantine、Ant Design。
9.6.5 Depth Tiers
| 层级 | 可观察能力 |
|---|---|
| L0 | 知道 React / JSX 是什么 |
| L1 | 看得懂 props / state 示例 |
| L2 | 能写受控表单、自定义 Hook、错误边界 |
| L3 | 能解释 key、lift state、状态位置决策 |
| L4 | 能设计组件库、a11y 焦点流、组合 API |
本计划目标:L2~L3。
9.6.6 Source
- react.dev:官方文档。
- TypeScript Handbook: JSX。
- 引用快照:2026-07-30。
10. 常见误区
- 用
index做 key - 受控与非受控混用
- 把 useState 当同步读
- 列表里直接
setState修改对象引用(不算 immutable) - 不知道 ErrorBoundary 不捕事件
- 不写 prop 类型
- 透传 5 层 props 才开始用 Context
- Modal 关闭不归还焦点
<div onClick>替代<button>- 觉得 React.Fragment 多余。
11. 所有知识点分类
- 编程语言 2. 数据结构与算法 3. 计算机基础 4. 工程技术 5. Web 与后端 6. 前端与客户端 7. 数据与人工智能 8. 项目与职业能力 9. 安全与可靠性
本计划归属:前端与客户端 主 + 工程技术 辅。