🐸 青蛙编程网
首页 / 前端 / React / 8. 综合练习:React 版待办清单

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 拓展练习

  1. localStorage 持久化:用 useEffect + [tasks] 存取
  2. 筛选项:全部/未完成/已完成(state 加 filter, useMemo 可选)
  3. 提取 useInput 小 hook,管理 input 的 value/onChange

8.6 小结

React 基础完成:

  • 组件 = 函数,UI = f(state)
  • useState / useEffect / 事件三件套
  • 不可变更新 + map列表 + 回调通信

前端板块至此有了 jQuery、Vue、React 三门兵器。接下来离开浏览器,进入终端与服务器世界——从 Linux 基础 开始 🐸

📥 下载本章代码

ZIP 内含本章全部代码文件与说明,解压即用,与教程内容同步生成。