React 入门可以围绕一张任务列表练习:组件负责显示,状态保存变化,事件把用户操作转成状态更新。你可能已经会写 JSX,却发现输入框内容改了,页面列表没有跟着变化,或者删除一项后勾选状态跑到了另一项。问题通常在数据如何保存和识别,而不是标签写得不够多。

本文完成添加、勾选和删除三个动作,解释它们为什么都应从状态出发。示例使用 React 18.3.1 的浏览器开发构建,让初学者直接打开页面实验;这只是固定版本的教学环境,不是生产项目的部署建议。所有任务保存在页面内存,刷新后清空。
一、组件应表达一段完整界面
组件可以理解成接收数据、返回界面的一段函数。任务项组件接收任务对象和两个回调,知道怎样显示这一项,却不自行维护全局任务列表。列表属于父组件,父组件决定添加或删除后有哪些任务。
props 是父组件传给子组件的输入。不要在子组件里直接修改传入的任务对象,因为父组件仍持有原来的状态引用。把用户意图通过回调传回去,由持有状态的组件更新,数据来源会更清楚。
如果组件、属性和渲染还不熟悉,可以先读 React 教程。先理解组件边界,再考虑拆文件;文件数量并不能证明组件设计合理。
二、把完整任务列表跑起来
保存为 tasks.html,在可访问示例脚本服务的环境中打开。为减少额外工具,代码使用 React.createElement,它与 JSX 描述的元素关系相同,只是写法不同。
<!doctype html>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>编程狮 React 任务练习</title>
<script src="https://cdn.jsdelivr.net/npm/react@18.3.1/umd/react.development.js"></script>
<script src="https://cdn.jsdelivr.net/npm/react-dom@18.3.1/umd/react-dom.development.js"></script>
<div id="root"></div>
<script>
const h = React.createElement;
function TaskItem({ task, onToggle, onRemove }) {
return h('li', null,
h('label', null,
h('input', { type: 'checkbox', checked: task.done,
onChange: () => onToggle(task.id) }), task.text),
h('button', { type: 'button', onClick: () => onRemove(task.id) }, '删除'));
}
function App() {
const [text, setText] = React.useState('');
const [tasks, setTasks] = React.useState([]);
const nextId = React.useRef(1);
function add(event) {
event.preventDefault();
const value = text.trim();
if (!value) return;
const task = { id: nextId.current++, text: value, done: false };
setTasks(previous => [...previous, task]);
setText('');
}
function toggle(id) {
setTasks(previous => previous.map(task =>
task.id === id ? { ...task, done: !task.done } : task));
}
function remove(id) {
setTasks(previous => previous.filter(task => task.id !== id));
}
const done = tasks.filter(task => task.done).length;
return h('main', null,
h('h1', null, '今天的学习任务'),
h('form', { onSubmit: add },
h('label', { htmlFor: 'task' }, '新任务'),
h('input', { id: 'task', value: text, maxLength: 80,
onChange: event => setText(event.target.value) }),
h('button', { type: 'submit' }, '添加')),
h('p', { role: 'status' }, `共 ${tasks.length} 项,完成 ${done} 项`),
h('ul', null, tasks.map(task => h(TaskItem,
{ key: task.id, task, onToggle: toggle, onRemove: remove }))));
}
ReactDOM.createRoot(document.querySelector('#root')).render(h(App));
</script>
先输入“学习组件”并回车,再输入“练习状态”并点击添加。计数应变成两项。勾选第二项后完成数量为一,删除第一项后应只剩第二项,并保持勾选。
输入框的 value 来自 text 状态,onChange 把用户输入交给 setText,构成受控输入。仅给 value 却不更新状态,输入框看起来就会像无法编辑。任务为空白时不添加,是 add 里的业务检查,与按钮是否可点击是两个层次。
三、状态更新为什么要创建新数组
useState 返回当前值与更新函数。你直接 tasks.push 修改旧数组,并不能表达一次可靠的状态替换。示例添加时用展开语法创建新数组,删除时用 filter,勾选时用 map 找到目标后创建新对象。
数组复制不是深复制。只复制外层后再修改内部对象,旧状态里的同一个对象也会被改动。因此 toggle 同时创建新数组和被修改任务的新对象,未修改任务可以继续复用原对象。
更新依赖旧状态时,示例采用函数形式,把 previous 交给计算过程。这避免你根据某次渲染里捕获的旧数组计算下一步。回调应保持纯粹;本例把新 id 的分配放在事件函数中,而不是塞进状态更新回调。
数组 map、filter 与展开语法可结合 JavaScript 教程 理解。不要一遇到 React 更新问题就归因于框架;很多问题来自普通对象共享和数组修改方式。
四、列表身份与派生数据怎样处理
key 用于帮助 React 识别同一列表中的项目身份。任务删除、插入后,稳定 id 仍表示同一个任务;数组下标则表示当前位置。把位置当身份,在含输入或局部状态的列表里容易产生错位。
示例用递增计数生成本次页面会话中的 id,并用 useRef 保存计数器。它不会因为重新渲染而回到一。真实系统若有服务端任务 id,应优先使用业务身份;如果需要跨设备合并,本地递增数并不够用。
已完成数量通过 tasks 计算,不再额外保存一个 completedCount 状态。两个可以相互推导的数据分别维护,会增加同步错误:删除一个已完成任务时,列表变了,计数忘记减一。当前规模下直接计算足够清楚,不必先引入复杂优化。
| 用户动作 | 状态变化 | 需要保持的关系 |
|---|---|---|
| 添加任务 | 新数组增加对象 | id 不与现有任务冲突 |
| 勾选任务 | 替换目标对象 | 其他任务值保持原样 |
| 删除任务 | 过滤目标 id | 剩余任务身份不变 |
如果页面空白,先检查两个脚本是否成功加载,再看控制台错误。若界面出现但点击异常,检查事件回调与状态数据。不要把脚本加载失败和组件逻辑错误混在一起处理。

总结
删除操作还可以检查一次对象身份:先添加两条相同文字的任务,勾选其中一条,再删除另一条。剩余任务应保持自己的勾选状态,这说明程序按 id 而非文字寻找目标。若需求禁止重复任务,应在添加阶段增加业务判断,不能借用 key 规则替代去重。
React 入门的重点是状态所有权与更新路径:父组件持有任务数组,子组件接收数据并回传动作,列表用稳定 id 表达身份。界面上的计数是任务数组的计算结果,不需要另一份独立记录。
把三步操作跑通后,可以增加“只看已完成”的筛选功能。先保存筛选条件,再从原任务数组计算显示列表,不要把筛选结果覆盖原数组。这样切回全部时任务仍在,也能检验你是否真正理解了源数据和展示结果的区别。
延伸学习
- React 入门实例教程 可帮助梳理组件与状态基本概念。
- React 入门课程 可作为继续完成交互练习的学习路径。
- React 点击事件说明 可辅助理解事件回调的组织。
常见问题
Q:为什么 setTasks 后立刻打印仍可能是旧值?
当前事件函数读取的是本次渲染取得的状态。调用更新函数不会把已有变量立即改成新快照,应观察下一次渲染的界面或相应调试输出。
Q:任务文字相同能不能当成同一个 key?
不能保证。两条任务可以有相同文字,却是不同对象。身份应独立于可编辑的显示内容,用唯一且稳定的 id 更合适。
Q:刷新后任务消失是不是状态失效了?
不是。本例只保存内存状态,刷新会重新创建应用。持久化到本地存储或服务器属于下一步功能,要另行设计读取和保存过程。
技术依据:React 状态数组更新指南。

免费 AI IDE



