React从基础入门到高级实战:React 实战项目 - 项目一:在线待办事项应用

React 实战项目:在线待办事项应用

欢迎来到本 React 开发教程专栏的第 26 篇!在之前的 25 篇文章中,我们从 React 的基础概念逐步深入到高级技巧,涵盖了组件、状态、路由和性能优化等核心知识。这一次,我们将通过一个完整的实战项目------在线待办事项应用,将这些知识融会贯通,帮助您从理论走向实践。

本项目的目标是为初学者提供一个简单但全面的 React 开发体验。通过这个项目,您将学习如何分析需求、选择技术栈、实现功能并最终将应用部署到线上。无论您是刚刚接触 React 的新手,还是希望通过实践巩固基础的开发者,这篇文章都将为您提供清晰的指引和丰富的代码示例。


引言

React 是一个强大的前端框架,其声明式编程和组件化特性让开发者能够高效地构建用户界面。然而,仅仅理解理论是不够的------真正的学习发生在实践中。在本项目中,我们将构建一个在线待办事项应用,这是一个经典的入门案例,既简单又实用,能够帮助您掌握 React 的核心技能。

这个应用的目标非常明确:允许用户创建、编辑和删除待办事项,支持按状态过滤,并将数据保存在本地,确保刷新页面后不会丢失。我们将从需求分析开始,逐步完成技术选型、代码实现和部署上线,并在最后提供一个练习,帮助您进一步巩固所学内容。

通过这个项目,您将体验到:

  • 组件化思维:如何将复杂的界面拆分为可重用的模块。
  • 状态管理:如何在应用中高效地共享和更新数据。
  • 路由设计:如何实现多页面导航。
  • 数据持久化:如何使用本地存储保存用户数据。

准备好了吗?让我们开始吧!


需求分析

在动手写代码之前,我们需要明确这个待办事项应用的具体功能需求。一个清晰的需求清单不仅能指导开发过程,还能帮助我们理解每个功能的意义。以下是我们项目的核心需求:

  1. 创建待办事项
    用户可以输入任务描述并添加到待办列表中。
  2. 编辑待办事项
    用户可以修改已有任务的内容。
  3. 删除待办事项
    用户可以移除不再需要的任务。
  4. 过滤待办事项
    用户可以根据任务状态(全部、已完成、未完成)筛选列表。
  5. 数据持久化
    数据将保存在浏览器本地存储中,刷新页面后依然可用。

为什么选择这些功能?

这些功能覆盖了待办事项应用的核心场景,同时也为学习 React 提供了丰富的实践机会:

  • 创建和编辑涉及表单处理和事件监听。
  • 删除和过滤需要掌握状态更新和数组操作。
  • 本地存储引入了数据持久化的概念。

此外,这些功能简单直观,非常适合初学者上手,同时也为后续扩展(如添加分类、优先级等)留下了空间。


技术栈选择

在开始实现之前,我们需要选择合适的技术栈。以下是本项目使用的工具和技术,以及选择它们的理由:

  • React
    核心框架,用于构建用户界面。React 的组件化和声明式编程让开发过程更加直观。
  • Vite
    构建工具,提供快速的开发服务器和高效的打包能力。相比传统的 Create React App,Vite 的启动速度更快,热更新体验更优。
  • React Router
    用于实现页面导航。虽然待办事项应用可以是单页应用,但我们将通过多页面设计展示路由的用法。
  • Context API
    React 内置的状态管理工具,用于在组件间共享待办事项数据。相比 Redux,它更轻量,适合小型项目。

技术栈的优势

  • React:生态丰富,学习曲线平滑,是现代前端开发的标配。
  • Vite:2025 年的前端开发趋势偏向轻量化和高性能,Vite 代表了这一方向。
  • React Router:支持动态路由和参数传递,是多页面应用的理想选择。
  • Context API:无需引入外部依赖,简单易用,适合初学者理解状态管理。

这些工具的组合不仅易于上手,还能帮助您掌握现代 React 开发的精髓。


项目实现

现在,我们进入最核心的部分------代码实现。我们将从项目搭建开始,逐步完成组件拆分、路由设计、状态管理和本地存储的开发。

1. 项目搭建

首先,使用 Vite 创建一个新的 React 项目:

bash 复制代码
npm create vite@latest todo-app -- --template react
cd todo-app
npm install
npm run dev

安装必要的依赖:

bash 复制代码
npm install react-router-dom

这将启动一个基础的 React 项目,接下来我们将逐步实现功能。

2. 组件拆分

组件化是 React 的核心思想。通过将应用拆分为多个小组件,我们可以提高代码的可读性和复用性。

组件结构
  • App:根组件,负责路由配置和整体布局。
  • TodoList:显示待办事项列表,包含过滤功能。
  • TodoItem:展示单个待办事项,支持编辑和删除。
  • TodoForm:用于添加或编辑待办事项的表单。
  • FilterButtons:提供状态过滤选项。
文件结构
复制代码
src/
├── components/
│   ├── TodoList.jsx
│   ├── TodoItem.jsx
│   ├── TodoForm.jsx
│   └── FilterButtons.jsx
├── context/
│   └── TodoContext.jsx
├── App.jsx
└── main.jsx

3. 路由设计

我们将应用设计为多页面结构,使用 React Router 实现导航。

路由配置
  • /:首页,显示待办事项列表。
  • /add:添加待办事项页面。
  • /edit/:id:编辑指定待办事项页面。

App.jsx 中配置路由:

js 复制代码
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import TodoList from './components/TodoList';
import TodoForm from './components/TodoForm';
import { TodoProvider } from './context/TodoContext';

function App() {
  return (
    <TodoProvider>
      <Router>
        <div className="min-h-screen bg-gray-100 p-4">
          <Routes>
            <Route path="/" element={<TodoList />} />
            <Route path="/add" element={<TodoForm />} />
            <Route path="/edit/:id" element={<TodoForm />} />
          </Routes>
        </div>
      </Router>
    </TodoProvider>
  );
}

export default App;
导航链接

TodoList 中添加导航到添加页面的按钮:

js 复制代码
import { Link } from 'react-router-dom';

function TodoList() {
  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">待办事项</h1>
      <Link to="/add" className="bg-blue-500 text-white px-4 py-2 rounded">
        添加任务
      </Link>
      {/* 列表内容 */}
    </div>
  );
}

export default TodoList;

4. 状态管理

我们使用 Context API 管理全局状态,包括待办事项列表和过滤条件。

创建 Context

src/context/TodoContext.jsx 中:

js 复制代码
import { createContext, useState, useEffect } from 'react';

export const TodoContext = createContext();

export function TodoProvider({ children }) {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');

  // 加载本地存储数据
  useEffect(() => {
    const storedTodos = JSON.parse(localStorage.getItem('todos')) || [];
    setTodos(storedTodos);
  }, []);

  // 保存数据到本地存储
  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  return (
    <TodoContext.Provider value={{ todos, setTodos, filter, setFilter }}>
      {children}
    </TodoContext.Provider>
  );
}
使用 Context

TodoList 中访问和过滤数据:

js 复制代码
import { useContext } from 'react';
import { TodoContext } from '../context/TodoContext';
import TodoItem from './TodoItem';
import FilterButtons from './FilterButtons';

function TodoList() {
  const { todos, filter } = useContext(TodoContext);

  const filteredTodos = todos.filter((todo) => {
    if (filter === 'completed') return todo.completed;
    if (filter === 'incomplete') return !todo.completed;
    return true;
  });

  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">待办事项</h1>
      <Link to="/add" className="bg-blue-500 text-white px-4 py-2 rounded mb-4 inline-block">
        添加任务
      </Link>
      <FilterButtons />
      <ul className="space-y-2">
        {filteredTodos.map((todo) => (
          <TodoItem key={todo.id} todo={todo} />
        ))}
      </ul>
    </div>
  );
}

export default TodoList;

5. 组件实现

TodoItem

TodoItem.jsx 中实现单个待办事项的展示和操作:

js 复制代码
import { useContext } from 'react';
import { Link } from 'react-router-dom';
import { TodoContext } from '../context/TodoContext';

function TodoItem({ todo }) {
  const { todos, setTodos } = useContext(TodoContext);

  const toggleComplete = () => {
    setTodos(
      todos.map((t) =>
        t.id === todo.id ? { ...t, completed: !t.completed } : t
      )
    );
  };

  const deleteTodo = () => {
    setTodos(todos.filter((t) => t.id !== todo.id));
  };

  return (
    <li className="flex items-center justify-between p-2 bg-white rounded shadow">
      <div className="flex items-center">
        <input
          type="checkbox"
          checked={todo.completed}
          onChange={toggleComplete}
          className="mr-2"
        />
        <span className={todo.completed ? 'line-through text-gray-500' : ''}>
          {todo.text}
        </span>
      </div>
      <div>
        <Link
          to={`/edit/${todo.id}`}
          className="text-blue-500 mr-2"
        >
          编辑
        </Link>
        <button onClick={deleteTodo} className="text-red-500">
          删除
        </button>
      </div>
    </li>
  );
}

export default TodoItem;
TodoForm

TodoForm.jsx 中实现添加和编辑表单:

js 复制代码
import { useState, useContext, useEffect } from 'react';
import { useParams, useNavigate } from 'react-router-dom';
import { TodoContext } from '../context/TodoContext';

function TodoForm() {
  const { todos, setTodos } = useContext(TodoContext);
  const { id } = useParams();
  const navigate = useNavigate();
  const [text, setText] = useState('');

  useEffect(() => {
    if (id) {
      const todo = todos.find((t) => t.id === id);
      if (todo) setText(todo.text);
    }
  }, [id, todos]);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;

    if (id) {
      // 编辑
      setTodos(
        todos.map((t) => (t.id === id ? { ...t, text } : t))
      );
    } else {
      // 添加
      const newTodo = {
        id: Date.now().toString(),
        text,
        completed: false,
      };
      setTodos([...todos, newTodo]);
    }
    navigate('/');
  };

  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">
        {id ? '编辑任务' : '添加任务'}
      </h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
          className="w-full p-2 border rounded mb-4"
          placeholder="请输入任务描述"
        />
        <button
          type="submit"
          className="bg-blue-500 text-white px-4 py-2 rounded"
        >
          保存
        </button>
      </form>
    </div>
  );
}

export default TodoForm;
FilterButtons

FilterButtons.jsx 中实现过滤按钮:

js 复制代码
import { useContext } from 'react';
import { TodoContext } from '../context/TodoContext';

function FilterButtons() {
  const { filter, setFilter } = useContext(TodoContext);

  return (
    <div className="mb-4">
      <button
        onClick={() => setFilter('all')}
        className={`mr-2 px-4 py-2 rounded ${
          filter === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200'
        }`}
      >
        全部
      </button>
      <button
        onClick={() => setFilter('completed')}
        className={`mr-2 px-4 py-2 rounded ${
          filter === 'completed' ? 'bg-blue-500 text-white' : 'bg-gray-200'
        }`}
      >
        已完成
      </button>
      <button
        onClick={() => setFilter('incomplete')}
        className={`px-4 py-2 rounded ${
          filter === 'incomplete' ? 'bg-blue-500 text-white' : 'bg-gray-200'
        }`}
      >
        未完成
      </button>
    </div>
  );
}

export default FilterButtons;

6. 本地存储

本地存储已在 TodoContext 中实现,通过 localStorage 保存和加载数据,确保刷新页面后数据不会丢失。


部署

开发完成后,我们将应用部署到 Netlify,让它在线上运行。

1. 构建项目

运行以下命令生成静态文件:

bash 复制代码
npm run build

这会生成 dist 文件夹,包含应用的静态资源。

2. 部署到 Netlify

  1. 注册 Netlify :访问 Netlify 官网 并创建账号。
  2. 新建站点:在控制台选择"New site from Git"。
  3. 连接仓库:将项目推送至 GitHub 并连接。
  4. 配置构建
    • 构建命令:npm run build
    • 发布目录:dist
  5. 部署:点击"Deploy site",等待部署完成。

部署成功后,您将获得一个唯一的 URL,可以通过它访问您的待办事项应用。


练习:添加分类功能

为了帮助您巩固所学,我们设计了一个练习:为待办事项添加分类功能。

需求

  • 用户可以为任务指定分类(如"工作"、"个人"、"学习")。
  • 用户可以按分类过滤任务。

实现步骤

  1. 扩展数据结构
    todos 中为每个任务添加 category 字段。
  2. 更新 TodoForm
    添加分类选择下拉菜单。
  3. 更新过滤逻辑
    TodoListFilterButtons 中支持分类过滤。

示例代码

修改 TodoContext
js 复制代码
export function TodoProvider({ children }) {
  const [todos, setTodos] = useState([]);
  const [filter, setFilter] = useState('all');

  useEffect(() => {
    const storedTodos = JSON.parse(localStorage.getItem('todos')) || [];
    setTodos(storedTodos);
  }, []);

  useEffect(() => {
    localStorage.setItem('todos', JSON.stringify(todos));
  }, [todos]);

  return (
    <TodoContext.Provider value={{ todos, setTodos, filter, setFilter }}>
      {children}
    </TodoContext.Provider>
  );
}
修改 TodoForm
js 复制代码
function TodoForm() {
  const { todos, setTodos } = useContext(TodoContext);
  const { id } = useParams();
  const navigate = useNavigate();
  const [text, setText] = useState('');
  const [category, setCategory] = useState('工作');

  useEffect(() => {
    if (id) {
      const todo = todos.find((t) => t.id === id);
      if (todo) {
        setText(todo.text);
        setCategory(todo.category);
      }
    }
  }, [id, todos]);

  const handleSubmit = (e) => {
    e.preventDefault();
    if (!text.trim()) return;

    if (id) {
      setTodos(
        todos.map((t) =>
          t.id === id ? { ...t, text, category } : t
        )
      );
    } else {
      const newTodo = {
        id: Date.now().toString(),
        text,
        category,
        completed: false,
      };
      setTodos([...todos, newTodo]);
    }
    navigate('/');
  };

  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">
        {id ? '编辑任务' : '添加任务'}
      </h1>
      <form onSubmit={handleSubmit}>
        <input
          type="text"
          value={text}
          onChange={(e) => setText(e.target.value)}
          className="w-full p-2 border rounded mb-4"
          placeholder="请输入任务描述"
        />
        <select
          value={category}
          onChange={(e) => setCategory(e.target.value)}
          className="w-full p-2 border rounded mb-4"
        >
          <option value="工作">工作</option>
          <option value="个人">个人</option>
          <option value="学习">学习</option>
        </select>
        <button
          type="submit"
          className="bg-blue-500 text-white px-4 py-2 rounded"
        >
          保存
        </button>
      </form>
    </div>
  );
}
修改 TodoList 和 FilterButtons
js 复制代码
function TodoList() {
  const { todos, filter } = useContext(TodoContext);

  const filteredTodos = todos.filter((todo) => {
    if (filter === 'completed') return todo.completed;
    if (filter === 'incomplete') return !todo.completed;
    if (['工作', '个人', '学习'].includes(filter)) return todo.category === filter;
    return true;
  });

  return (
    <div>
      <h1 className="text-2xl font-bold mb-4">待办事项</h1>
      <Link to="/add" className="bg-blue-500 text-white px-4 py-2 rounded mb-4 inline-block">
        添加任务
      </Link>
      <FilterButtons />
      <ul className="space-y-2">
        {filteredTodos.map((todo) => (
          <TodoItem key={todo.id} todo={todo} />
        ))}
      </ul>
    </div>
  );
}

function FilterButtons() {
  const { filter, setFilter } = useContext(TodoContext);

  return (
    <div className="mb-4 flex flex-wrap gap-2">
      <button
        onClick={() => setFilter('all')}
        className={`px-4 py-2 rounded ${
          filter === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200'
        }`}
      >
        全部
      </button>
      <button
        onClick={() => setFilter('completed')}
        className={`px-4 py-2 rounded ${
          filter === 'completed' ? 'bg-blue-500 text-white' : 'bg-gray-200'
        }`}
      >
        已完成
      </button>
      <button
        onClick={() => setFilter('incomplete')}
        className={`px-4 py-2 rounded ${
          filter === 'incomplete' ? 'bg-blue-500 text-white' : 'bg-gray-200'
        }`}
      >
        未完成
      </button>
      <button
        onClick={() => setFilter('工作')}
        className={`px-4 py-2 rounded ${
          filter === '工作' ? 'bg-blue-500 text-white' : 'bg-gray-200'
        }`}
      >
        工作
      </button>
      <button
        onClick={() => setFilter('个人')}
        className={`px-4 py-2 rounded ${
          filter === '个人' ? 'bg-blue-500 text-white' : 'bg-gray-200'
        }`}
      >
        个人
      </button>
      <button
        onClick={() => setFilter('学习')}
        className={`px-4 py-2 rounded ${
          filter === '学习' ? 'bg-blue-500 text-white' : 'bg-gray-200'
        }`}
      >
        学习
      </button>
    </div>
  );
}

练习目标

通过这个练习,您将学会如何扩展现有功能,提升对状态管理和组件通信的理解。


注意事项

  • 初学者友好:本文避免了复杂的概念,所有代码都尽量保持简单直观。
  • 学习建议:建议您边阅读边动手实现,遇到问题时查阅 React 官方文档和Vite 文档。
  • 扩展思路:完成项目后,可以尝试添加更多功能,如任务优先级、截止日期或提醒功能。

结语

通过这个在线待办事项应用项目,您从需求分析到部署上线,完整地走过了一个 React 项目的开发流程。您学习了组件拆分、路由设计、状态管理和数据持久化等核心技能,这些知识将成为您未来开发更复杂应用的基础。

相关推荐
崔庆才丨静觅8 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby60619 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了9 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅9 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅9 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅10 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment10 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅10 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊10 小时前
jwt介绍
前端
爱敲代码的小鱼10 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax