《React 入门实战:从零搭建 TodoList》

React 入门实战:从零搭建 TodoList(父子通信+本地存储+Stylus)

作为 React 入门的经典案例,TodoList 几乎涵盖了 React 基础开发中最核心的知识点------组件拆分、父子组件通信、响应式状态管理、本地存储持久化,再搭配 Stylus 预处理和 Vite 构建,既能夯实基础,又能贴近实际开发场景。

本文将基于完整可运行代码,一步步拆解 React TodoList 的实现逻辑,重点讲解父子组件通信的核心技巧、本地存储的优雅实现,以及组件化开发的最佳实践,适合 React 新手入门学习,也适合作为基础复盘素材。

一、项目环境与技术栈

先明确本次实战的技术栈组合,都是前端开发中高频使用的工具,简单易上手:

  • 构建工具:Vite(替代 Webpack,启动更快、打包更高效,适合中小型项目快速开发)
  • 核心框架 :React(使用 Hooks 语法,useState 管理组件状态,useEffect 处理副作用)
  • 样式预处理:Stylus(比 CSS 更简洁,支持嵌套、变量、混合等特性,提升样式开发效率)
  • 本地存储:localStorage(实现 Todo 数据持久化,刷新页面数据不丢失)

项目初始化命令(快速搭建基础环境):

bash 复制代码
# 初始化 Vite + React 项目
npm create vite@latest react-todo-demo -- --template react
# 进入项目目录
cd react-todo-demo
# 安装依赖
npm install
# 安装 Stylus(样式预处理)
npm install stylus --save-dev
# 启动项目
npm run dev

二、项目结构与组件拆分

组件化是 React 开发的核心思想,一个清晰的项目结构能提升代码可读性和可维护性。本次 TodoList 我们拆分为 4 个核心组件,遵循「单一职责原则」,每个组件只负责自己的功能:

bash 复制代码
src/
├── components/       # 自定义组件目录
│   ├── TodoInput.js  # 输入框组件:添加新 Todo
│   ├── TodoList.js   # 列表组件:展示所有 Todo、切换完成状态、删除 Todo
│   └── TodoStats.js  # 统计组件:展示 Todo 总数、活跃数、已完成数,清空已完成
├── styles/           # 样式目录
│   └── app.styl      # 全局样式(使用 Stylus 编写)
└── App.js            # 根组件:管理全局状态、协调所有子组件

核心逻辑:根组件 App 作为「数据中心」,持有所有 Todo 数据和修改数据的方法,通过 props 将数据和方法传递给子组件;子组件不直接修改数据,只能通过父组件传递的方法提交修改请求,实现数据统一管理。

三、核心功能实现(附完整代码解析)

下面从根组件到子组件,一步步解析每个功能的实现逻辑,重点讲解父子通信、状态管理和本地存储的核心细节。

3.1 根组件 App.js:数据中心与组件协调

App 组件是整个 TodoList 的核心,负责:初始化 Todo 数据、定义修改数据的方法、监听数据变化并持久化到本地存储、传递数据和方法给子组件。

javascript 复制代码
import { useState, useEffect } from 'react'
import './styles/app.styl'
import TodoList from './components/TodoList'
import TodoInput from './components/TodoInput'
import TodoStats from './components/TodoStats'

function App() {
  // 1. 初始化 Todo 数据(本地存储持久化)
  // useState 高级用法:传入函数,避免每次渲染都执行 JSON.parse(性能优化)
  const [todos, setTodos] = useState(() => {
    const saved = localStorage.getItem('todos');
    // 本地存储有数据则解析,无数据则初始化为空数组
    return saved ? JSON.parse(saved) : [];
  })

  // 2. 定义修改数据的方法(供子组件调用)
  // 新增 Todo:接收子组件传递的文本,添加到 todos 数组
  const addTodo = (text) => {
    // 注意:React 状态不可直接修改,需通过扩展运算符创建新数组
    setTodos([...todos, {
      id: Date.now(), // 用时间戳作为唯一 ID,简单高效
      text,           // 子组件传入的 Todo 文本
      completed: false, // 初始状态为未完成
    }])
  }

  // 删除 Todo:接收子组件传递的 ID,过滤掉对应 Todo
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id))
  }

  // 切换 Todo 完成状态:接收 ID,修改对应 Todo 的 completed 属性
  const toggleTodo = (id) => {
    setTodos(todos.map(todo => 
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ))
  }

  // 清空已完成 Todo:过滤掉所有 completed 为 true 的 Todo
  const clearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed))
  }

  // 3. 计算统计数据(传递给 TodoStats 组件)
  const activeCount = todos.filter(todo => !todo.completed).length; // 活跃 Todo 数
  const completedCount = todos.filter(todo => todo.completed).length; // 已完成 Todo 数

  // 4. 副作用:监听 todos 变化,持久化到本地存储
  // 依赖数组 [todos]:只有 todos 变化时,才执行该函数
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]) 

  // 5. 渲染子组件,通过 props 传递数据和方法
  return (
    My Todo List
      {/* 输入框组件:传递 addTodo 方法,用于新增 Todo */}
<TodoInput onAdd={addTodo}/>
      {/* 列表组件:传递 todos 数据,以及删除、切换状态的方法 */}
      <TodoList 
        todos={todos} 
        onDelete={deleteTodo}
        onToggle={toggleTodo}
      />
      {/* 统计组件:传递统计数据和清空方法 */}<TodoStats 
        total={todos.length}
        active={activeCount}
        completed={completedCount}
        onClearCompleted={clearCompleted}
      />
    
  )
}

export default App

关键知识点解析:

  • useState 高级用法 :传入函数初始化状态,避免每次组件渲染都执行 JSON.parse,提升性能(尤其数据量大时)。
  • 状态不可变 :React 状态是只读的,修改 todos 时,必须通过 filtermap、扩展运算符等方式创建新数组,不能直接修改原数组(如 todos.push(...) 是错误写法)。
  • useEffect 副作用:监听 todos 变化,将数据存入 localStorage,实现「刷新页面数据不丢失」;依赖数组 [todos] 确保只有数据变化时才执行存储操作,避免无效渲染。
  • 父子通信基础:父组件通过 props 向子组件传递数据(如 todos)和方法(如 addTodo),子组件通过调用这些方法修改父组件的状态。

3.2 子组件 1:TodoInput.js(输入框组件)

负责接收用户输入的 Todo 文本,通过父组件传递的 onAdd 方法,将文本提交给父组件,实现新增 Todo 功能。

javascript 复制代码
import { useState } from 'react'

const TodoInput = (props) => {
  // 接收父组件传递的 addTodo 方法
  const { onAdd } = props;

  // 本地状态:管理输入框的值(React 单向绑定)
  const [inputValue, setInputValue] = useState('');

  // 处理表单提交:新增 Todo
  const handleSubmit = (e) => {
    e.preventDefault(); // 阻止表单默认提交行为(避免页面刷新)
    // 简单校验:输入不能为空
    if (!inputValue.trim()) return;
    // 调用父组件传递的方法,提交输入的文本
    onAdd(inputValue);
    // 清空输入框
    setInputValue('');
  }

  return (
    <form className="todo-input" onSubmit={<input 
        type="text" 
        value={绑定:输入框的值由 inputValue 控制
        onChange={e => setInputValue(e.target.value)} // 监听输入变化,更新状态
        placeholder="请输入 Todo..."
      />
      
  )
}

export default TodoInput

关键知识点解析:

  • React 单向绑定 :React 不支持 Vue 中的 v-model 双向绑定(为了性能优化,避免不必要的视图更新),通过「value + onChange」实现数据与视图的同步------输入框的值由 inputValue 控制,输入变化时通过 onChange 更新 inputValue
  • 子父通信 :子组件通过调用父组件传递的 onAdd 方法,将输入的文本传递给父组件,实现「子组件向父组件传递数据」(核心:父传方法,子调用方法传参)。
  • 表单校验:简单的非空校验,避免添加空 Todo,提升用户体验。

3.3 子组件 2:TodoList.js(列表组件)

负责展示所有 Todo 列表,接收父组件传递的 todos 数据,以及删除、切换完成状态的方法,实现 Todo 列表的渲染、状态切换和删除功能。

ini 复制代码
const TodoList = (props) => {
  // 接收父组件传递的数据和方法
  const { todos, onDelete, onToggle } = props;

  return (
    
      {
        // 空状态处理:没有 Todo 时显示提示
        todos.length === 0 ? (
          No todos yet!
        ) : (
          // 遍历 todos 数组,渲染每个 Todo 项
          todos.map(todo => (
            <li 
              key={唯一 key,React 用于优化渲染(避免重复渲染)
              className={todo.completed ? 'completed' : ''} // 根据完成状态添加样式
            >{todo.text}
              {/* 删除按钮:点击时调用 onDelete 方法,传递当前 Todo 的 ID */}<button onClick={ onDelete(todo.id)}>X
          ))
        )
      }
    
  )
}

export default TodoList

关键知识点解析:

  • 列表渲染 :使用 map 遍历 todos 数组,渲染每个 Todo 项;必须添加 key 属性(推荐用唯一 ID),React 通过 key 识别列表项的变化,优化渲染性能。
  • 条件渲染:判断 todos 数组长度,为空时显示「No todos yet!」,提升空状态体验。
  • 状态切换与删除 :复选框的 checked 属性绑定 todo.completed,点击时调用 onToggle 方法传递 Todo ID;删除按钮点击时调用 onDelete 方法传递 ID,实现子组件触发父组件数据修改。

3.4 子组件 3:TodoStats.js(统计组件)

负责展示 Todo 统计信息(总数、活跃数、已完成数),以及清空已完成 Todo 的功能,接收父组件传递的统计数据和清空方法。

javascript 复制代码
const TodoStats = (props) => {
  // 接收父组件传递的统计数据和清空方法
  const { total, active, completed, onClearCompleted } = props;

  return (
    
      {/* 展示统计信息 */}
     Total: {total} | Active: {active} | Completed: {completed} {
        // 条件渲染:只有已完成数 > 0 时,显示清空按钮
        completed > 0 && (
          <button 
            onClick={            className="clear-btn"
          >Clear Completed
        )
      }
    
  )
}

export default TodoStats

关键知识点解析:

  • 条件渲染优化:只有当已完成 Todo 数大于 0 时,才显示「Clear Completed」按钮,避免按钮无效显示,提升用户体验。
  • 父子通信复用:和其他子组件一样,通过 props 接收父组件的方法(onClearCompleted),点击按钮时调用,触发父组件清空已完成 Todo 的操作。

3.5 样式文件 app.styl(Stylus 编写)

使用 Stylus 编写全局样式,利用嵌套、变量等特性,简化样式编写,提升可维护性(示例代码):

yaml 复制代码
// 定义变量(可复用)
$primary-color = #42b983
$gray-color = #f5f5f5
$completed-color = #999

.todo-app
  max-width: 600px
  margin: 2rem auto
  padding: 0 1rem
  font-family: 'Arial', sans-serif

.todo-input
  display: flex
  gap: 0.5rem
  margin-bottom: 1.5rem
  input
    flex: 1
    padding: 0.5rem
    border: 1px solid #ddd
    border-radius: 4px
  button
    padding: 0.5rem 1rem
    background: $primary-color
    color: white
    border: none
    border-radius: 4px
    cursor: pointer

.todo-list
  list-style: none
  padding: 0
  margin: 0 0 1.5rem 0
  li
    display: flex
    justify-content: space-between
    align-items: center
    padding: 0.8rem
    margin-bottom: 0.5rem
    background: white
    border-radius: 4px
    box-shadow: 0 2px 4px rgba(0,0,0,0.1)
    &.completed
      span
        text-decoration: line-through
        color: $completed-color
    label
      display: flex
      align-items: center
      gap: 0.5rem
    button
      background: #ff4444
      color: white
      border: none
      border-radius: 50%
      width: 20px
      height: 20px
      display: flex
      align-items: center
      justify-content: center
      cursor: pointer
  .empty
    text-align: center
    padding: 1rem
    color: $gray-color
    font-style: italic

.todo-stats
  display: flex
  justify-content: space-between
  align-items: center
  padding: 0.8rem
  background: $gray-color
  border-radius: 4px
  .clear-btn
    padding: 0.3rem 0.8rem
    background: #ff4444
    color: white
    border: none
    border-radius: 4px
    cursor: pointer

四、核心知识点总结(重点!)

通过这个 TodoList 案例,我们掌握了 React 基础开发的核心技能,尤其是父子组件通信和状态管理,这也是 React 开发中最常用的知识点,总结如下:

4.1 父子组件通信(核心)

React 中组件通信的核心是「单向数据流」,即数据从父组件流向子组件,子组件通过调用父组件传递的方法修改数据,具体分为两种情况:

  1. 父传子 :通过 props 传递数据(如 todos、total、active)和方法(如 addTodo、onDelete),子组件通过 props.xxx 接收使用。
  2. 子传父:父组件传递一个方法给子组件,子组件调用该方法时传递参数,父组件通过方法参数接收子组件的数据(如 TodoInput 传递输入文本给 App)。

4.2 兄弟组件通信(间接实现)

React 中没有直接的兄弟组件通信方式,需通过「父组件作为中间媒介」实现:

例如 TodoInput(新增 Todo)和 TodoList(展示 Todo)是兄弟组件,它们的通信流程是:TodoInput → 调用父组件 addTodo 方法传递文本 → 父组件更新 todos 状态 → 父组件通过 props 将更新后的 todos 传递给 TodoList → TodoList 重新渲染。

4.3 状态管理与本地存储

  • 使用 useState 管理组件状态,遵循「状态不可变」原则,修改状态必须通过 setXXX 方法,且不能直接修改原状态。
  • 使用 useEffect 处理副作用(如本地存储),依赖数组控制副作用的执行时机,避免无效渲染。
  • localStorage 持久化:将 todos 数据存入本地存储,页面刷新时从本地存储读取数据,实现数据不丢失(注意:localStorage 只能存储字符串,需用 JSON.stringifyJSON.parse 转换)。

4.4 组件化开发最佳实践

  • 单一职责原则:每个组件只负责一个功能(如 TodoInput 只负责输入,TodoList 只负责展示)。
  • 复用性:组件设计时尽量通用,避免硬编码(如 TodoList 不关心 Todo 的具体内容,只负责渲染和触发方法)。
  • 用户体验:添加空状态、表单校验、条件渲染等细节,提升用户使用体验。

五、最终效果与扩展方向

5.1 最终效果

  • 输入文本,点击 Add 按钮新增 Todo。
  • 点击复选框,切换 Todo 完成状态(已完成显示删除线)。
  • 点击 Todo 项右侧的 X,删除对应的 Todo。
  • 底部显示 Todo 统计信息,已完成数大于 0 时显示清空按钮。
  • 刷新页面,所有 Todo 数据不丢失(本地存储生效)。

5.2 扩展方向(进阶练习)

如果想进一步提升,可以尝试以下扩展功能,巩固 React 基础:

  • 添加 Todo 编辑功能(双击 Todo 文本可编辑)。
  • 添加筛选功能(全部、活跃、已完成)。
  • 使用 useReducer 替代 useState 管理复杂状态(适合 todos 操作较多的场景)。
  • 添加动画效果(如 Todo 新增/删除时的过渡动画)。
  • 使用 Context API 实现跨组件状态共享(替代 props 层层传递)。

六、总结

TodoList 虽然是 React 入门案例,但涵盖了 React 开发中最核心的知识点------组件拆分、父子通信、状态管理、副作用处理、本地存储,以及 Stylus 预处理和 Vite 构建的使用。

对于 React 新手来说,建议亲手敲一遍完整代码,重点理解「单向数据流」和「父子组件通信」的逻辑,再尝试扩展功能,逐步夯实 React 基础。

✨ 附:项目运行命令

bash 复制代码
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 打包构建(部署用)
npm run build
相关推荐
Jing_Rainbow1 小时前
【React-10/Lesson94(2026-01-04)】React 性能优化专题:useMemo & useCallback 深度解析🚀
前端·javascript·react.js
无巧不成书02182 小时前
React Native 深度解析:跨平台移动开发框架
javascript·react native·react.js·华为·开源·harmonyos
2301_796512523 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:订单步骤条实践
javascript·react native·react.js·ecmascript·harmonyos
程序员酥皮蛋3 小时前
react 01 初学react
前端·javascript·react.js
全马必破三3 小时前
Vue 和 React 的区别
前端·vue.js·react.js
灵犀坠3 小时前
React+Node.js全栈实战:实现安全高效的博客封面图片上传(踩坑实录)
安全·react.js·node.js·router·query·clerk
无巧不成书02183 小时前
React Native 鸿蒙开发(RNOH)深度适配
前端·javascript·react native·react.js·前端框架·harmonyos
2301_796512523 小时前
【精通篇】打造React Native鸿蒙跨平台开发高级复合组件库开发系列:Tag 标签(通过 type 属性控制标签颜色)
javascript·react native·react.js·ecmascript·harmonyos
程序哥聊面试3 小时前
第一课:React的Hooks
前端·javascript·react.js