REACT · Vol.I · LESSON 08 · 核心原语
第一章实战:手写一个待办清单组件
用「后端思维」先拆需求
第一章学了这么多概念,是时候把它们拧成一股绳了。我们要写一个待办清单(Todo),功能不花哨,但恰好覆盖本章全部知识点:
- 一个输入框,可以新增待办;
- 每条待办可以勾选完成、删除;
- 底部显示剩余未完成数。
用你熟悉的「后端拆接口」套路,先想清楚数据模型和状态,别急着写 UI:
// 一条待办的数据结构
type Todo = {
id: number; // 唯一标识(就是 list 的 key)
text: string; // 内容
done: boolean; // 是否完成
};
// 组件需要的 state
// 1. todos: Todo[] —— 待办列表
// 2. input: string —— 输入框当前文字
「先定 state、再写 JSX」是最重要的分水岭动作。就像后端「先建表结构、再写 Controller」——state 就是前端的「表结构」。state 定清楚了,界面只是它的投影(还记得第 1 篇的 UI = f(state) 吗?)。
顺带验证一下上一站学的「哪些数据不配进 state」:剩余条数能由 todos 推导,不进;输入框内容会被用户持续修改、且参与渲染,进。
五个动作,逐个攻破
整个组件其实就是 5 个「状态操作」,每个都对应一个知识点:
import { useState } from "react";
type Todo = { id: number; text: string; done: boolean };
// 计数器放在组件外:模块只加载一次,不会随重渲染重置
let nextId = 1;
function TodoApp() {
// ── 状态(先定 state)──
const [todos, setTodos] = useState<Todo[]>([]);
const [input, setInput] = useState("");
// ── ① 新增:不可变更新,append 新项 ──
function addTodo() {
const text = input.trim();
if (!text) return; // 空输入拦截(对应后端参数校验)
setTodos([...todos, { id: nextId++, text, done: false }]);
setInput(""); // 清空输入框
}
// ── ② 勾选完成:map 置换目标项 ──
function toggle(id) {
setTodos(todos.map(t =>
t.id === id ? { ...t, done: !t.done } : t
));
}
// ── ③ 删除:filter 过滤目标项 ──
function remove(id) {
setTodos(todos.filter(t => t.id !== id));
}
// ── ④ 派生值:不要存进 state,直接算 ──
const remaining = todos.filter(t => !t.done).length;
// ── ⑤ 视图:状态 → 界面 ──
return (
<div className="todo">
<form onSubmit={e => { e.preventDefault(); addTodo(); }}>
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="输入待办…"
/>
<button type="submit">添加</button>
</form>
<ul>
{todos.map(t => (
<li key={t.id}>
<label>
<input
type="checkbox"
checked={t.done}
onChange={() => toggle(t.id)}
/>
<span style={{ textDecoration: t.done ? "line-through" : "none" }}>
{t.text}
</span>
</label>
<button onClick={() => remove(t.id)}>删除</button>
</li>
))}
</ul>
<p>剩余 {remaining} 项</p>
</div>
);
}
建议你在本地把它跑起来,依次验证:回车能添加(onSubmit 在发力)、空内容添加不了、勾选后文字划线、底部数字跟着变。
let nextId = 1 的位置——它在组件函数外面。如果写进函数里,每次重渲染都会被重置回 1,新条目的 id 就会和旧条目撞车,key 一重复,第 7 篇讲的所有灾难都会上演。这是「变量作用域」这个最基础的知识在 React 里的投影:组件函数会反复执行,想跨渲染保存的值,要么交给 state / ref,要么放到函数外面。真实项目里,id 通常来自后端数据库;纯前端临时用 crypto.randomUUID() 也很稳。这段代码里藏着的「第一章全部精华」
把它和前面 7 篇一一对应,你会看到每一篇都不是白学的:
| 代码位置 | 对应知识点 | 来源章节 |
|---|---|---|
return (<div>...</div>) | 声明式:只写「状态 → 界面」的映射 | 第 1 篇 |
<input value={input} onChange=...> | JSX:{} 插值、驼峰属性、受控组件 | 第 2 篇 |
function TodoApp() / 无任何 props | 组件:一个函数 | 第 3 篇 |
useState + [...todos, x] 不可变更新 | State:不直接 mutate,返回新数组 | 第 4 篇 |
remaining = todos.filter(...) | 派生值不存 state(渲染模型的「纯函数」思想) | 第 4、5 篇 |
onSubmit + preventDefault、onChange={() => toggle(t.id)} | 事件:拦截默认行为、箭头函数传参 | 第 6 篇 |
key={t.id} | 列表 + key:稳定唯一标识 | 第 7 篇 |
进阶:把 TodoItem 拆成子组件
上面的版本能跑,但列表项的 JSX 还嵌在大组件里。按第 3 篇的标准——「这块能用一句话描述吗?会被复用吗?」——TodoItem 完全够格独立成组件。拆:
function TodoItem({ todo, onToggle, onRemove }) {
return (
<li>
<label>
<input
type="checkbox"
checked={todo.done}
onChange={() => onToggle(todo.id)}
/>
<span style={{ textDecoration: todo.done ? "line-through" : "none" }}>
{todo.text}
</span>
</label>
<button onClick={() => onRemove(todo.id)}>删除</button>
</li>
);
}
<ul>
{todos.map(t => (
<TodoItem key={t.id} todo={t} onToggle={toggle} onRemove={remove} />
))}
</ul>
停下来品一品这次拆分里的每个决定,它们全是前几篇讲过的规矩:
- key 挂在
TodoItem上——它是map直接生成的最外层元素(第 7 篇); - 子组件自己不持有任何 state——数据全部来自父组件的 props,自己只是「投影」;
- 想改数据?调用传进来的
onToggle/onRemove——数据向下流、动作向上传,子组件永远不越权改数据(第 3 篇)。
这就是前端版的「Controller 不含业务逻辑,只做转发」:TodoItem 是无状态的展示层,所有「写操作」都上抛给持有数据的父组件处理。谁持有数据谁负责改——职责边界清清楚楚,这正是卷二「状态提升」要系统化的东西,你已经提前用上了。
给自己留三道「课后题」
把这个 Todo 组件在你自己的项目里跑起来,然后依次完成三个小目标:
- 加「全部完成」按钮:一键把所有
done设为true(练map批量置换)。
提示:setTodos(todos.map(t => ({ ...t, done: true })))。 - 加「编辑」功能:双击待办文字变成输入框,回车保存(练「受控组件 + 局部状态」)。
提示:在 TodoItem 里加一个editingstate 控制两种样子,别忘了第 4 篇的「界面有几种样子」。 - 加「筛选」功能:全部 / 未完成 / 已完成三个页签(练「派生值」)。
提示:新 state 只有一个filter;展示的列表是todos+filter的派生值,就地filter出来再map,别存进 state。
第一章总结
你已经完成了从「后端思维」到「React 心智」的第一次完整跨越:
- 声明式:
UI = f(state),先穷举状态再画映射; - JSX:HTML 的外壳、JavaScript 的骨,条件渲染三板斧;
- 组件 + Props:函数 + 不可变单向参数,数据向下、动作向上;
- State:组件字段,不可变更新、函数式更新、快照心智;
- 渲染模型:虚拟 DOM 三阶段,重渲染 ≠ 重写 DOM;
- 事件 + 列表 key:委托分发 + 稳定身份。
这些就是 React 的「地基」。第 2 卷开始,我们进入真正让 React 强大的部分——Hooks 精讲:先从那个坑倒无数人的 useEffect 开始。
Comments · 评论