8. 综合练习:React 版待办清单
这是什么:综合练习 = 用 React 第三次重做同一个「待办清单」需求,把本教程的组件、状态、列表、通信全部用上。
为什么用:同一件事做过三遍,你才分得清哪些是 jQuery、Vue、React 各自的写法,哪些是三种框架共用的「数据先行」思路。
如何用:本章先写一个单组件版把功能跑通,再拆出 TaskRow 练习回调通信,最后对照三框架差异,并指一条通往正式项目的工程化路径。
8.1 单组件版(先跑通)
整段可跑,一个文件就是完整应用:
<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='utf-8'>
<title>React 待办清单</title>
<style>
.app { max-width: 420px; margin: 40px auto; font-family: 'Microsoft YaHei', sans-serif; }
.app input { padding: 10px; width: 60%; }
.app button { padding: 10px 16px; background: #4caf50; color: #fff; border: none; cursor: pointer; }
.app ul { padding: 0; list-style: none; }
.app li { padding: 12px; margin-top: 8px; background: #f9f9f9; border-radius: 6px; cursor: pointer; }
.app li.done { text-decoration: line-through; color: #999; background: #eef7ee; }
.app .x { float: right; background: none; border: none; color: #f44336; font-size: 16px; }
</style>
<script src='https://registry.npmmirror.com/react/18.3.1/files/umd/react.production.min.js'></script>
<script src='https://registry.npmmirror.com/react-dom/18.3.1/files/umd/react-dom.production.min.js'></script>
<script src='https://registry.npmmirror.com/@babel/standalone/7.29.11/files/babel.min.js'></script>
</head>
<body>
<div id='root'></div>
<script type='text/babel' data-presets='react'>
let nextId = 3
function App() {
// 数据(唯一真相)
const [tasks, setTasks] = React.useState([
{ id: 1, text: '学 React', done: false },
{ id: 2, text: '写教程', done: true }
])
const [newTask, setNewTask] = React.useState('')
// 操作:都是不可变更新
function addTask() {
const text = newTask.trim()
if (!text) return
setTasks([...tasks, { id: nextId++, text, done: false }])
setNewTask('')
}
function removeTask(id) {
setTasks(tasks.filter((t) => t.id !== id))
}
function toggleDone(id) {
setTasks(tasks.map((t) =>
t.id === id ? { ...t, done: !t.done } : t
))
}
// 派生数据:直接算(相当于 computed)
const doneCount = tasks.filter((t) => t.done).length
const percent = tasks.length
? Math.round((doneCount / tasks.length) * 100)
: 0
// UI
return (
<div className='app'>
<h1>React 待办清单</h1>
<input
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addTask()}
placeholder='输入任务'
/>
<button onClick={addTask}>添加</button>
<p>已完成 {doneCount} / {tasks.length}({percent}%)</p>
<ul>
{tasks.map((t) => (
<li key={t.id} className={t.done ? 'done' : ''}
onClick={() => toggleDone(t.id)}>
{t.text}
<button className='x'
onClick={(e) => { e.stopPropagation(); removeTask(t.id) }}>
×
</button>
</li>
))}
</ul>
</div>
)
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
</script>
</body>
</html>
你会看到:和 jQuery、Vue 版同一个待办清单——输入任务回车或点「添加」新增,点条目切换完成划线,点「×」删除,顶部实时统计完成率。
8.2 进阶:拆成父子组件
把 li 拆出 TaskRow,练习回调通信(上一章模式)。整段可跑,行为与 8.1 完全一致:
<!DOCTYPE html>
<html lang='zh-CN'>
<head>
<meta charset='utf-8'>
<title>React 待办清单(父子组件版)</title>
<style>
.app { max-width: 420px; margin: 40px auto; font-family: 'Microsoft YaHei', sans-serif; }
.app input { padding: 10px; width: 60%; }
.app button { padding: 10px 16px; background: #4caf50; color: #fff; border: none; cursor: pointer; }
.app ul { padding: 0; list-style: none; }
.app li { padding: 12px; margin-top: 8px; background: #f9f9f9; border-radius: 6px; cursor: pointer; }
.app li.done { text-decoration: line-through; color: #999; background: #eef7ee; }
.app .x { float: right; background: none; border: none; color: #f44336; font-size: 16px; }
</style>
<script src='https://registry.npmmirror.com/react/18.3.1/files/umd/react.production.min.js'></script>
<script src='https://registry.npmmirror.com/react-dom/18.3.1/files/umd/react-dom.production.min.js'></script>
<script src='https://registry.npmmirror.com/@babel/standalone/7.29.11/files/babel.min.js'></script>
</head>
<body>
<div id='root'></div>
<script type='text/babel' data-presets='react'>
let nextId = 4
function TaskRow({ task, onToggle, onDelete }) { // 子:显示 + 递话
return (
<li className={task.done ? 'done' : ''} onClick={() => onToggle(task.id)}>
{task.text}
<button className='x'
onClick={(e) => { e.stopPropagation(); onDelete(task.id) }}>
×
</button>
</li>
)
}
function App() { // 父:唯一改数据的地方
const [tasks, setTasks] = React.useState([
{ id: 1, text: '学 React', done: false },
{ id: 2, text: '写教程', done: true },
{ id: 3, text: '跑步三公里', done: false }
])
const [newTask, setNewTask] = React.useState('')
function addTask() {
const text = newTask.trim()
if (!text) return
setTasks([...tasks, { id: nextId++, text, done: false }])
setNewTask('')
}
function toggleDone(id) {
setTasks(tasks.map((t) => t.id === id ? { ...t, done: !t.done } : t))
}
function removeTask(id) {
setTasks(tasks.filter((t) => t.id !== id))
}
const doneCount = tasks.filter((t) => t.done).length
return (
<div className='app'>
<h1>React 待办清单</h1>
<input
value={newTask}
onChange={(e) => setNewTask(e.target.value)}
onKeyDown={(e) => e.key === 'Enter' && addTask()}
placeholder='输入任务'
/>
<button onClick={addTask}>添加</button>
<p>已完成 {doneCount} / {tasks.length}</p>
<ul>
{tasks.map((t) => (
<TaskRow key={t.id} task={t} onToggle={toggleDone} onDelete={removeTask} />
))}
</ul>
</div>
)
}
ReactDOM.createRoot(document.getElementById('root')).render(<App />)
</script>
</body>
</html>
你会看到:一样的待办清单,多了一条「跑步三公里」当初始数据。差别全在代码组织上——渲染的归子组件,改数据的归父组件。
8.3 三框架同一实现对照
| 关键点 | jQuery | Vue | React |
|---|---|---|---|
| 增任务 | append('<li>') |
tasks.push |
setTasks([...tasks, x]) |
| 删任务 | 找 DOM fadeOut+remove | splice |
filter |
| 完成 | toggleClass | task.done = ! |
map + {...t, done:!t.done} |
| 派生统计 | 手工重算塞 DOM | computed | 渲染中直接算 |
| 回车 | e.key 判断 |
@keyup.enter |
onKeyDown + e.key |
| 阻止冒泡 | stopPropagation() |
.stop |
事件对象手工调 |
心法:「数据先行」三框架完全一致;落地方式:Vue 靠可变,React 用不可变。哪个写起来最顺手并不重要,理解到这一层,以后接触任何前端方案都在你的扩展半径里。
8.4 工程化路径(进阶了解)
学习阶段免构建够用;正式项目的姿势是「提前编译」——JSX 在开发期就被编译好,浏览器只拿结果:
npm create vite@latest my-react-app -- --template react
cd my-react-app
npm install
npm run dev
Vite 会给你 JSX 文件、组件目录与热更新,React 版本也能随时用上最新。把 8.2 的组件搬进 src/App.jsx(钩子从 react 包 import,不用再解构全局),即完成从「学习页」到「工程项目」的迁移。
8.5 拓展练习
- localStorage 持久化:用 useEffect +
[tasks]存取 - 筛选项:全部/未完成/已完成(state 加
filter, useMemo 可选) - 提取 useInput 小 hook,管理 input 的 value/onChange
8.6 小结
React 基础完成:
- 组件 = 函数,UI = f(state)
- useState / useEffect / 事件三件套
- 不可变更新 + map列表 + 回调通信
前端板块至此有了 jQuery、Vue、React 三门兵器。接下来离开浏览器,进入终端与服务器世界——从 Linux 基础 开始 🐸