使用 React + Stylus + Vite 构建一个完整的 Todo 应用

44 阅读4分钟

使用 React + Stylus + Vite 构建一个完整的 Todo 应用

在现代前端开发中,React 以其组件化、声明式和高效的特性成为构建用户界面的首选框架。本文将带你从零开始,使用 React + Stylus + Vite 搭建一个功能完整、结构清晰的 Todo List 应用,并深入讲解 父子组件通信、子父通信、兄弟组件通信 的最佳实践。


技术栈说明

  • React:用于构建 UI 的 JavaScript 库。
  • Vite:新一代前端构建工具,启动快、热更新迅速。
  • Stylus:一种富有表现力、动态且健壮的 CSS 预处理器,简化样式编写。
  • localStorage:用于持久化存储 Todo 数据,页面刷新后依然保留。

项目结构概览

src/
├── App.jsx
├── components/
│   ├── TodoInput.jsx
│   ├── TodoList.jsx
│   └── TodoStats.jsx
└── styles/
    └── app.styl

核心思想:状态提升(Lifting State Up)

在本项目中,所有共享状态(todos)都由 父组件 App 统一管理。子组件通过 props 接收数据回调函数 来实现交互。这种模式确保了:

  • 数据流单向、可预测;
  • 状态集中管理,便于调试与维护;
  • 子组件无状态(或仅持有局部 UI 状态),职责单一。

父组件 App.jsx:状态管理中心

import { useState, useEffect } from 'react'
import './styles/app.styl'
import TodoInput from './components/TodoInput'
import TodoList from './components/TodoList'
import TodoStats from './components/TodoStats'

function App() {
  // 初始化 todos,优先从 localStorage 读取
  const [todos, setTodos] = useState(() => {
    const saved = localStorage.getItem('todos');
    return saved ? JSON.parse(saved) : [];
  });

  // 添加 Todo
  const addTodo = (text) => {
    setTodos([...todos, {
      id: Date.now(),
      text,
      completed: false
    }]);
  };

  // 删除 Todo
  const deleteTodo = (id) => {
    setTodos(todos.filter(todo => todo.id !== id));
  };

  // 切换完成状态
  const toggleTodo = (id) => {
    setTodos(todos.map(todo =>
      todo.id === id ? { ...todo, completed: !todo.completed } : todo
    ));
  };

  // 清除已完成项
  const clearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed));
  };

  // 计算统计信息
  const activeCount = todos.filter(todo => !todo.completed).length;
  const completedCount = todos.filter(todo => todo.completed).length;

  // 持久化到 localStorage
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  return (
    <div className="todo-app">
      <h1>My Todos List</h1>
      <TodoInput onAdd={addTodo} />
      <TodoList 
        todos={todos} 
        onDelete={deleteTodo}
        onToggle={toggleTodo}
      />
      <TodoStats 
        total={todos.length}
        active={activeCount}
        completed={completedCount}
        onClearCompleted={clearCompleted}
      />
    </div>
  );
}

export default App;

关键点解析:

  • useState 初始化时使用函数形式:避免每次渲染都解析 localStorage。
  • useEffect 监听 todos 变化:自动同步到本地存储。
  • 方法作为 props 传递:子组件通过调用这些函数“请求”修改状态,而非直接操作。

子组件详解

1. TodoInput.jsx:输入新任务

import { useState } from 'react';

const TodoInput = ({ onAdd }) => {
  const [inputValue, setInputValue] = useState('');

  const handleSubmit = (e) => {
    e.preventDefault();
    if (inputValue.trim()) {
      onAdd(inputValue.trim());
      setInputValue('');
    }
  };

  return (
    <form className="todo-input" onSubmit={handleSubmit}>
      <input 
        type="text"
        value={inputValue}
        onChange={e => setInputValue(e.target.value)} 
        placeholder="What needs to be done?"
      />
      <button type="submit">Add</button>
    </form>
  );
};

export default TodoInput;

注意:React 是单向数据流,通过 onChange + value 实现受控组件,确保视图与状态同步。


2. TodoList.jsx:展示与操作任务列表

const TodoList = ({ todos, onDelete, onToggle }) => {
  return (
    <ul className="todo-list">
      {todos.length === 0 ? (
        <li className="empty">No todos yet!</li>
      ) : (
        todos.map(todo => (
          <li key={todo.id} className={todo.completed ? 'completed' : ''}>
            <label>
              <input 
                type="checkbox"
                checked={todo.completed}
                onChange={() => onToggle(todo.id)}
              />
              <span>{todo.text}</span>
            </label>
            <button onClick={() => onDelete(todo.id)}>×</button>
          </li>
        ))
      )}
    </ul>
  );
};

export default TodoList;

每个 Todo 项通过 onToggleonDelete 向父组件“上报”用户操作。


3. TodoStats.jsx:统计与清理

const TodoStats = ({ total, active, completed, onClearCompleted }) => {
  return (
    <div className="todo-stats">
      <p>Total: {total} | Active: {active} | Completed: {completed}</p>
      {completed > 0 && (
        <button onClick={onClearCompleted} className="clear-btn">
          Clear Completed
        </button>
      )}
    </div>
  );
};

export default TodoStats;

仅当有已完成项时才显示“清除”按钮,提升用户体验。


兄弟组件如何通信?

本项目中,TodoInputTodoListTodoStats 是兄弟组件。它们并不直接通信,而是:

  1. 所有数据来自共同父组件 Apptodos 状态;
  2. 所有修改请求通过 App 提供的回调函数发起;
  3. App 更新状态后,自动重新渲染所有子组件。

✅ 这就是 “通过父组件间接通信” 的经典模式,也是 React 官方推荐的方式。


样式处理:Stylus 的优雅

app.styl 示例(部分):

.todo-app
  max-width 600px
  margin 40px auto
  padding 20px
  border 1px solid #eee
  border-radius 8px
  font-family Arial, sans-serif

.todo-input
  display flex
  gap 10px
  margin-bottom 20px
  input
    flex 1
    padding 8px
    border 1px solid #ccc
    border-radius 4px

.todo-list
  list-style none
  padding 0
  li
    display flex
    justify-content space-between
    align-items center
    padding 10px
    border-bottom 1px solid #f0f0f0
    &.completed span
      text-decoration line-through
      color #888

Stylus 的嵌套语法让 CSS 更具可读性和组织性。


总结

通过这个 Todo 应用,我们掌握了:

  • ✅ React 组件化开发的核心思想;
  • ✅ 父子通信:props 传递数据 + 回调函数;
  • ✅ 兄弟通信:状态提升至共同父组件;
  • ✅ 使用 useEffect 实现副作用(如 localStorage 同步);
  • ✅ Vite + Stylus 快速搭建现代化开发环境。

这个项目虽小,却涵盖了 React 开发中的关键模式与最佳实践,是学习状态管理和组件协作的绝佳范例。

🎄 圣诞快乐!愿你的代码永远没有 bug,Todo 永远能完成!