⚛️ React 19 组件化实战 ------ 从零搭建 Todo List 并吃透组件通信
前言 :上篇文章我们通过
state-demo用户列表案例深入剖析了useState的核心用法。本篇文章将更进一步,带你从零搭建一个完整的 Todo List 多组件应用,彻底搞懂 React 组件化开发中最关键的几个概念:组件拆分、Props 传递、状态提升、不可变数据更新、表单处理、条件渲染 。同时我们还会通过一段原生 JS 的DocumentFragment代码,帮你建立对 React 批处理底层原理的直觉。全文 9000+ 字,建议收藏后在电脑上边读边敲。
📦 一、项目总览:从单组件到多组件架构
1.1 与上一篇文章的承接
上一篇文章的 state-demo 项目,所有逻辑都塞在一个 App.jsx 里。这在学习阶段没问题,但真实项目中,动辄几百行的"巨石组件"是维护噩梦。todos 项目展示了正确的组件拆分方式:
css
state-demo(上篇) todos(本篇)
───────────────────── ───────────────────────────
src/ src/
├── main.jsx ├── main.jsx
├── App.jsx ← 所有逻辑在这里 ├── App.jsx ← 状态管理中心
├── App.css ├── App.css
└── index.css ├── index.css
└── components/ ← 关注点分离
├── TodoInput.jsx
├── TodoList.jsx
└── TodoStats.jsx
1.2 组件树拓扑图
🎯 核心规律 :状态(State)只存在于
App中 ,通过 Props 向下流动;子组件通过回调函数向上报告事件。这就是 React 的单向数据流。
🏗️ 二、App.jsx 深度剖析 ------ 状态管理中心
App.jsx 是整个应用的大脑,承载了所有业务状态和操作逻辑。
2.1 完整代码
jsx
import { useState } from 'react';
import TodoInput from './components/TodoInput.jsx';
import TodoList from './components/TodoList.jsx';
import TodoStats from './components/TodoStats.jsx';
import './App.css'
const App = () => {
const [todos, setTodos] = useState(() => [
{ id: 1, text: '学习React', completed: false },
{ id: 2, text: '睡觉', completed: false },
{ id: 3, text: '吃饭', completed: true },
]);
const addTodo = (text) => {
if (text.trim() === '') return;
setTodos([
{ id: +Date.now(), text, completed: false },
...todos,
]);
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed));
};
const activeCount = todos.filter(todo => !todo.completed).length;
const completedCount = todos.filter(todo => todo.completed).length;
return (
<div>
<h1>My Todo List</h1>
<TodoInput onAdd={addTodo} />
<TodoList todos={todos} onToggle={toggleTodo} onDelete={deleteTodo} />
<TodoStats
total={todos.length}
active={activeCount}
completed={completedCount}
onClearCompleted={clearCompleted}
/>
</div>
);
};
export default App;
2.2 两种组件定义方式:Function Declaration vs Arrow Function
jsx
// 上篇 state-demo:函数声明
function App() { ... }
// 本篇 todos:箭头函数
const App = () => { ... };
| 方式 | 代码 | 特点 |
|---|---|---|
| 函数声明 | function App() {} |
支持 hoisting(提升),可以在定义前引用 |
| 箭头函数 | const App = () => {} |
无自己的 this,更短,不可提升 |
💡 社区趋势:新项目越来越多使用箭头函数定义组件,因为它与
const配合更符合"组件是不可变引用"的语义。
2.3 初始状态设计:用真实数据模拟业务
jsx
const [todos, setTodos] = useState(() => [
{ id: 1, text: '学习React', completed: false },
{ id: 2, text: '睡觉', completed: false },
{ id: 3, text: '吃饭', completed: true },
]);
这里的三个设计要点:
| 字段 | 类型 | 说明 |
|---|---|---|
id |
number |
唯一标识,用于 key 和定位操作目标 |
text |
string |
待办事项内容 |
completed |
boolean |
完成状态,驱动 UI 样式和统计计算 |
🔑 关键设计 :
todos是一个对象数组 而不是简单字符串数组。这为后续的toggle(切换完成状态)和delete(删除)操作提供了必要的数据支撑。
🧩 三、不可变状态更新 ------ React 的核心哲学
⚠️ 最重要的原则 :React 中永远不要直接修改 state !必须返回一个全新的对象或数组。
3.1 addTodo:数组头部插入(Spread 运算符)
jsx
const addTodo = (text) => {
if (text.trim() === '') return; // 空白字符守卫
setTodos([
{ id: +Date.now(), text, completed: false }, // 新元素在前
...todos, // 展开旧数组在后
]);
};
逐步拆解:
vbnet
初始状态:[{id:1}, {id:2}, {id:3}]
调用 addTodo("新任务"):
Step 1: +Date.now() → 1690000000000(转数字的毫秒时间戳)
Step 2: 创建新对象 { id: 1690000000000, text: "新任务", completed: false }
Step 3: [...todos] 展开旧数组
Step 4: 拼成新数组 [{新}, {旧1}, {旧2}, {旧3}]
Step 5: setTodos(新数组) → React 检测到引用变化 → 重新渲染 ✅
技巧点:
+Date.now():Date.now()返回数字类型的毫秒时间戳,前面的+是冗余但显式的转数字操作,保证 ID 的唯一性(适合 Demo)text.trim() === '':边界守卫防止添加空白 todo,体现了防御式编程
3.2 toggleTodo:数组中某一项的更新(map + Spread)
jsx
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id
? { ...todo, completed: !todo.completed } // 匹配:浅拷贝 + 翻转
: todo // 不匹配:原样保留
));
};
这是 React 中最经典的数组元素更新模式,逐层分析:
bash
原数组:[{id:1, completed:false}, {id:2, completed:false}, {id:3, completed:true}]
调用 toggleTodo(2):
map 遍历:
id=1: 不匹配 → 返回原对象(引用不变)
id=2: 匹配! → { ...todo, completed: !false } → { id:2, completed: true }(新对象!)
id=3: 不匹配 → 返回原对象(引用不变)
结果:[原{id:1}, 新{id:2, completed:true}, 原{id:3}]
↑ 引用不变 ↑ 新引用 ↑ 引用不变
🎯 核心理解 :
{ ...todo, completed: !todo.completed }这行代码做的是浅拷贝 (Shallow Copy)------创建一个新对象,复制原对象的所有属性,然后覆盖completed字段。这种模式保证 React 能检测到引用变化,同时最小化不必要的对象创建。
3.3 deleteTodo:数组过滤(filter)
jsx
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
filter 天然返回新数组,是删除操作的最佳选择:
bash
原数组:[{id:1}, {id:2}, {id:3}]
调用 deleteTodo(2):
filter 条件:todo.id !== 2
{id:1} → true ✅ 保留
{id:2} → false ❌ 丢弃
{id:3} → true ✅ 保留
结果:[{id:1}, {id:3}]
常见陷阱对比:
jsx
// ❌ 错误:splice 直接修改原数组
todos.splice(index, 1);
setTodos(todos); // 引用没变,React 不更新!
// ❌ 错误:直接赋值修改属性
todos[0].completed = true;
setTodos(todos); // 引用没变,React 不更新!
// ✅ 正确:filter 返回新数组
setTodos(todos.filter(t => t.id !== id));
3.4 clearCompleted:批量删除(filter)
jsx
const clearCompleted = () => {
setTodos(todos.filter(todo => !todo.completed));
};
3.5 不可变更新模式速查表
| 操作 | 模式 | 核心 API |
|---|---|---|
| 新增 | 创建新数组,头/尾部插入 | [...old, newItem] 或 [newItem, ...old] |
| 修改某项 | map + 浅拷贝覆盖 | arr.map(item => match ? {...item, k: v} : item) |
| 删除某项 | 过滤掉目标 | arr.filter(item => item.id !== targetId) |
| 批量删除 | 过滤满足条件的 | arr.filter(item => !condition) |
| 清空 | 直接设空数组 | setArr([]) |
📝 四、TodoInput ------ 表单交互与父子通信
TodoInput.jsx 展示了 React 中表单处理的经典模式。
4.1 完整代码
jsx
import { useState } from 'react';
const TodoInput = ({ onAdd }) => {
const [inputValue, setInputValue] = useState('');
const handleSubmit = (e) => {
e.preventDefault(); // 阻止表单默认跳转行为
onAdd(inputValue); // 把数据"报告"给父组件
setInputValue(''); // 清空输入框
};
return (
<form className="todo-input" onSubmit={handleSubmit}>
<input
type="text"
autoFocus // 自动聚焦
placeholder="What needs to be done?"
value={inputValue} // 受控组件
onChange={(e) => setInputValue(e.target.value)}
/>
<button type="submit">Add</button>
</form>
);
};
export default TodoInput;
4.2 父子通信模型:Props 下传,事件上报
关键洞察:
- 父组件
App不知道子组件的表单细节(用的是什么输入方式、如何提交) - 子组件
TodoInput不知道父组件如何处理数据(存到哪里、怎么更新列表) - 它们之间唯一的约定是
onAdd(text)这个接口
这就是 React 组件设计的精髓:高内聚,低耦合。
4.3 受控组件模式详解
jsx
<input
value={inputValue} // ① 值由 React 状态控制
onChange={(e) => setInputValue(e.target.value)} // ② 输入变化 → 更新状态 → 重新渲染 → 更新 value
/>
受控组件的闭环数据流:
css
用户输入 "a"
↓
onChange 事件触发
↓
setInputValue("a")
↓
组件重新渲染
↓
<input value="a" /> ← 输入框显示 "a"
受控 vs 非受控:
| 受控组件 (Controlled) | 非受控组件 (Uncontrolled) | |
|---|---|---|
| 数据源 | React State | DOM 自身 |
| 获取值 | inputValue 变量 |
inputRef.current.value |
| 适用场景 | 实时验证、条件禁用、动态格式化 | 简单表单、文件上传 |
| 代码量 | 略多 | 更少 |
4.4 e.preventDefault() 为什么必不可少?
jsx
const handleSubmit = (e) => {
e.preventDefault(); // 🔑 阻止 <form> 的默认提交行为
// ...
};
<form onSubmit={...}> 中,如果没有 e.preventDefault():
- 浏览器会执行默认的表单提交
- 页面会刷新 或跳转 到
actionURL - 整个 React 应用被销毁重建
- 所有状态丢失
⚡ 记忆口诀 :React 中的
<form>,e.preventDefault()永远是第一行。
4.5 autoFocus 属性
jsx
<input autoFocus />
页面加载后,该输入框自动获得焦点,键盘可直接输入------提升用户体验的小细节。
📋 五、TodoList ------ 条件渲染与列表渲染
TodoList.jsx 是最能体现 React 声明式编程思想的组件。
5.1 完整代码
jsx
const TodoList = ({ todos, onToggle, onDelete }) => {
return (
<ul className="todo-list">
{
todos.length === 0 ? (
<li className="empty">No todos yet!</li>
) : (
todos.map(todo => (
<li key={todo.id}
className={todo.completed ? 'completed' : ''}>
<label>
<input type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
/>
</label>
<span>{todo.text}</span>
<button onClick={() => onDelete(todo.id)}>删除</button>
</li>
))
)
}
</ul>
);
};
export default TodoList;
5.2 条件渲染:三元表达式
jsx
todos.length === 0 ? (
<li className="empty">No todos yet!</li> // 👈 空状态
) : (
todos.map(...) // 👈 有数据
)
React 中条件渲染的 4 种方式:
| 方式 | 语法 | 适用场景 |
|---|---|---|
| 三元表达式 | cond ? <A/> : <B/> |
二选一(如空/非空) |
逻辑与 && |
cond && <A/> |
条件为真时显示 |
| if/else | 在函数体中提前 return | 复杂条件,提前返回 |
| 对象映射 | {key: <Comp/>}[status] |
多分支枚举 |
5.3 动态 className
jsx
<li className={todo.completed ? 'completed' : ''}>
配合 App.css 中的样式:
css
.completed {
text-decoration: line-through; /* 删除线 */
}
classnames 工具库版本(项目中常这样写):
jsx
import classnames from 'classnames';
<li className={classnames({ completed: todo.completed })}>
5.4 受控 Checkbox
jsx
<input type="checkbox"
checked={todo.completed} // 值由 Props 控制
onChange={() => onToggle(todo.id)} // 变化 → 通知父组件
/>
这是受控组件的又一个实例:
css
Checkbox 勾选
↓
onChange → onToggle(id)
↓
App: setTodos(map(...{ completed: !todo.completed }))
↓
todos 数组更新 → TodoList 重新渲染
↓
checked={新的 completed 值} → Checkbox 显示新状态
5.5 列表渲染的 key 属性再强调
jsx
todos.map(todo => (
<li key={todo.id}> {/* 使用业务唯一 ID,不要用 index */}
...
</li>
))
为什么不用 index 作为 key? 考虑这个场景:
vbnet
初始:[{id:1, text:"A"}, {id:2, text:"B"}]
key=0 key=1
删除 A 后:[{id:2, text:"B"}]
key=0
问题:React 看到 key=0 从 "A" 变成了 "B",
认为 DOM 节点可以复用,只更新文本内容。
但如果 B 的 <input> 中有未提交的用户输入,这个输入会错误地保留在 A 的位置!
📊 六、TodoStats ------ 纯展示组件的典范
TodoStats.jsx 是最简单的组件,但体现了重要的设计原则。
6.1 完整代码
jsx
const TodoStats = ({ total, active, completed, onClearCompleted }) => {
return (
<div className="todo-stats">
<p>Total: {total} | Active: {active} | Completed: {completed}</p>
{
completed > 0 && (
<button onClick={onClearCompleted} className="clear-btn">
Clear Completed
</button>
)
}
</div>
);
};
export default TodoStats;
6.2 逻辑与 && 条件渲染
jsx
{
completed > 0 && (
<button onClick={onClearCompleted}>Clear Completed</button>
)
}
这是 && 条件渲染的典型应用:
ini
completed = 3 → true → 渲染按钮 ✅
completed = 0 → false → 不渲染(短路求值) ❌
⚠️ 注意 :
0和NaN用&&时会被渲染到页面上!因为 React 会把0当作文本节点。安全写法:
jsx{completed > 0 ? <button ... /> : null} // ✅ 明确的三元 {!!completed && <button ... />} // ✅ 显式转布尔
6.3 "纯展示组件"的特点
TodoStats 是一个典型的无状态组件(Stateless Component):
- ✅ 没有自己的
useState - ✅ 只依赖 Props 渲染
- ✅ 不产生副作用
- ✅ 相同的 Props 永远渲染相同的 UI
这种组件:
- 易测试:给定 Props,断言渲染结果
- 易复用:不依赖特定上下文
- 易理解:输入输出明确
🔄 七、状态提升(Lifting State Up)------ 最重要的组件通信模式
7.1 为什么要把状态放在 App 中?
假设我们把 todos 放在 TodoList 组件中:
jsx
// ❌ 糟糕的设计
const TodoList = () => {
const [todos, setTodos] = useState([]);
// ...
};
// TodoInput 和 TodoStats 无法访问 todos!
问题 :TodoInput 需要添加 todo,TodoStats 需要统计 todo------它们都需要访问同一个 todos 数据。但 React 的 state 是组件私有的,无法被兄弟组件直接访问。
解决方案 :状态提升------将共享状态放到最近的公共祖先组件中。
css
❌ 各自为政(状态分散) ✅ 状态提升(单一数据源)
App App ← 状态在这里
/ \ / | \
Input List Input List Stats
[本地] [本地] 各自通过 Props 获取数据
7.2 单向数据流总览
7.3 数据流三定律
| 定律 | 描述 |
|---|---|
| 第一定律 | State 只能属于一个组件(Single Source of Truth) |
| 第二定律 | Props 是只读的,子组件不能修改 Props |
| 第三定律 | 子组件通过回调函数向上通知变化 |
记住这三条,React 组件通信就不会出错。
🌐 八、原生 JS 对照:DocumentFragment 与 React 批处理
在 text.html 中,有一段纯粹的原生 JS 代码,它揭示了 React 虚拟 DOM 批处理的思想源头。
8.1 完整代码
html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<ul id="list"></ul>
<script>
const data = ["任务1", "任务2", "任务3"];
const olist = document.querySelector('#list');
// 📦 文档碎片:没有实体标签,存在于内存中
const fragment = document.createDocumentFragment();
for (const task of data) {
const item = document.createElement('li'); // JS 运算(快)
item.innerText = task;
fragment.appendChild(item); // 添加到内存(不触发渲染)
}
// 🚀 一次性挂载,只触发一次页面绘制
olist.appendChild(fragment);
</script>
</body>
</html>
8.2 DocumentFragment 是什么?
DocumentFragment 是一个轻量级的"虚拟容器":
- 它没有实体 DOM 标签 (不像
<div>会多一层嵌套) - 它只存在于内存中
- 当它被插入 DOM 时,插入的是它的所有子节点,而不是它自身
8.3 逐次插入 vs 文档碎片 ------ 性能对比
javascript
// ❌ 方式一:逐次插入(每次插入触发一次回流 + 重绘)
for (const task of data) {
const item = document.createElement('li');
item.innerText = task;
olist.appendChild(item); // 第 1 次 → 回流 → 重绘
// 第 2 次 → 回流 → 重绘
// 第 3 次 → 回流 → 重绘
}
// ✅ 方式二:文档碎片(只触发一次回流 + 重绘)
const fragment = document.createDocumentFragment();
for (const task of data) {
const item = document.createElement('li');
item.innerText = task;
fragment.appendChild(item); // 在内存中操作,不触发渲染
}
olist.appendChild(fragment); // 🚀 一次性提交 → 只触发一次回流 + 重绘
ini
┌─────────────────────────────────────────────────┐
│ 浏览器渲染管线的代价 │
│ │
│ JS → Style → Layout → Paint → Composite │
│ ↑ ↑ │
│ 样式计算 绘制(最耗时) │
│ │
│ 逐次插入:Layout × 3 + Paint × 3 = 慢 │
│ 文档碎片:Layout × 1 + Paint × 1 = 快 │
└─────────────────────────────────────────────────┘
8.4 与 React 的类比
| 概念 | 原生 JS | React |
|---|---|---|
| 操作对象 | DocumentFragment | Virtual DOM |
| 计算位置 | 内存 | 内存 |
| 提交时机 | 手动 appendChild |
React 自动批量提交 |
| 优化效果 | 减少回流重绘 | 减少回流重绘 + Diff 最小化更新 |
💡 核心理解 :React 的
useState批处理机制(同一事件中多次 setState 只触发一次渲染),本质上和DocumentFragment的"攒起来一起提交"是同一个思想------减少与实际 DOM 的交互次数。
🧠 九、用对 React.memo 与 useCallback ------ 性能优化初探
当 Todo List 项目逐渐变复杂时,你可能会遇到不必要的子组件重渲染问题。
9.1 问题场景
jsx
// App 每次渲染都会创建新的函数引用
const addTodo = (text) => { ... }; // 新函数
const toggleTodo = (id) => { ... }; // 新函数
const deleteTodo = (id) => { ... }; // 新函数
// TodoInput 收到"新"的 onAdd → 认为 Props 变了 → 重渲染
<TodoInput onAdd={addTodo} />
9.2 解决方案
jsx
import { useState, useCallback } from 'react';
const App = () => {
const [todos, setTodos] = useState(...);
// ✅ useCallback 保持函数引用不变(依赖为空时)
// 但这里函数依赖 todos,所以每次 todos 变还是会更新引用
const addTodo = useCallback((text) => {
if (text.trim() === '') return;
setTodos(prev => [{ id: +Date.now(), text, completed: false }, ...prev]);
}, []); // 空依赖!因为用了函数式更新 prev => ...
// 同样优化 toggleTodo 和 deleteTodo
const toggleTodo = useCallback((id) => {
setTodos(prev => prev.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
}, []);
const deleteTodo = useCallback((id) => {
setTodos(prev => prev.filter(todo => todo.id !== id));
}, []);
// ...
};
关键升级:setTodos(count + 1) → setTodos(prev => ...)
使用函数式更新 prev => ... 后,函数不再依赖外部 todos 变量,因此 useCallback 的依赖数组可以为空 [],函数引用永远不变!
9.3 配合 React.memo
jsx
// TodoStats 是纯展示组件,用 memo 包裹
const TodoStats = React.memo(({ total, active, completed, onClearCompleted }) => {
console.log('TodoStats 渲染'); // 只在 Props 变化时打印
return (
<div className="todo-stats">
<p>Total: {total} | Active: {active} | Completed: {completed}</p>
{completed > 0 && <button onClick={onClearCompleted}>Clear Completed</button>}
</div>
);
});
React.memo 对传入的 Props 做浅比较,如果没变化就跳过渲染。
🎯 十、完整项目文件清单与运行指南
10.1 文件结构
bash
todos/
├── index.html # Vite 入口 HTML
├── package.json # React 19 + Vite 8
├── vite.config.js # Vite 配置(React 插件)
├── eslint.config.js # ESLint 扁平化配置
└── src/
├── main.jsx # 应用入口(createRoot)
├── App.jsx # 主组件(状态管理中心)
├── App.css # 组件样式(completed 删除线)
├── index.css # 全局样式(CSS 变量 + 暗色模式)
└── components/
├── TodoInput.jsx # 输入组件(受控 input + 表单)
├── TodoList.jsx # 列表组件(条件渲染 + map)
└── TodoStats.jsx # 统计组件(纯展示)
10.2 快速运行
bash
cd todos
npm install
npm run dev
# 浏览器打开 http://localhost:5173
📐 十一、知识体系总结
11.1 本文核心能力矩阵
python
┌──────────────────────────────────────────────────────────┐
│ React 组件化开发知识地图 │
├──────────────────────────────────────────────────────────┤
│ 🏗️ 组件架构 │
│ ├── 单组件 → 多组件拆分原则 │
│ ├── 状态提升(Lifting State Up) │
│ ├── 展示组件 vs 容器组件的区分 │
│ └── 组件树设计与 Props 接口定义 │
├──────────────────────────────────────────────────────────┤
│ 🔄 数据流 │
│ ├── 单向数据流:Props 下传,事件上报 │
│ ├── 不可变更新:spread / map / filter │
│ └── 回调函数作为 Props(onAdd / onToggle / onDelete) │
├──────────────────────────────────────────────────────────┤
│ 🎨 UI 模式 │
│ ├── 受控组件:input / checkbox │
│ ├── 条件渲染:三元 / && / 提前 return │
│ ├── 列表渲染:map + 稳定的 key │
│ └── 动态 className │
├──────────────────────────────────────────────────────────┤
│ ⚡ 性能理解 │
│ ├── DocumentFragment → Virtual DOM 思想对照 │
│ ├── useCallback 稳定函数引用 │
│ ├── React.memo 跳过不必要渲染 │
│ └── 函数式更新 prev => ... 减少依赖 │
└──────────────────────────────────────────────────────────┘
11.2 不可变更新速查(打印贴桌面版)
javascript
// 📌 新增
setTodos([newItem, ...todos]); // 头部插入
setTodos([...todos, newItem]); // 尾部追加
// 📌 修改某项
setTodos(todos.map(t => t.id === id
? { ...t, field: newValue } // 匹配:浅拷贝修改
: t // 不匹配:原样
));
// 📌 删除某项
setTodos(todos.filter(t => t.id !== id));
// 📌 批量删除
setTodos(todos.filter(t => !t.completed));
// 📌 对象类型的 state 同理
setUser({ ...user, name: 'newName' }); // 修改字段
setUser({ ...user, address: { ...user.address, city: 'HZ' } }); // 深层修改
11.3 下一步学习路线
| 序号 | 主题 | 说明 |
|---|---|---|
| 1 | useEffect |
副作用处理:数据请求、订阅、定时器 |
| 2 | useRef |
DOM 引用、保存可变值不触发渲染 |
| 3 | 自定义 Hook | 将本文的 useTodos 抽离为自定义 Hook |
| 4 | useReducer |
复杂状态逻辑的替代方案 |
| 5 | Context API | 跨层级数据传递,告别 Props Drilling |
| 6 | React 19 新特性 | useActionState、useOptimistic、use |
📝 写在最后 :从
state-demo的单个useState到todos的多组件协作,我们完成了 React 从"入门"到"能写项目"的关键一跃。如果上篇文章帮你理解了 React 的"细胞"(State),那这篇文章帮你建立了 React 的"组织架构"(组件通信)。建议将两篇文章对照阅读,效果更佳。