React 实战项目:在线待办事项应用
欢迎来到本 React 开发教程专栏的第 26 篇!在之前的 25 篇文章中,我们从 React 的基础概念逐步深入到高级技巧,涵盖了组件、状态、路由和性能优化等核心知识。这一次,我们将通过一个完整的实战项目------在线待办事项应用,将这些知识融会贯通,帮助您从理论走向实践。
本项目的目标是为初学者提供一个简单但全面的 React 开发体验。通过这个项目,您将学习如何分析需求、选择技术栈、实现功能并最终将应用部署到线上。无论您是刚刚接触 React 的新手,还是希望通过实践巩固基础的开发者,这篇文章都将为您提供清晰的指引和丰富的代码示例。
引言
React 是一个强大的前端框架,其声明式编程和组件化特性让开发者能够高效地构建用户界面。然而,仅仅理解理论是不够的------真正的学习发生在实践中。在本项目中,我们将构建一个在线待办事项应用,这是一个经典的入门案例,既简单又实用,能够帮助您掌握 React 的核心技能。
这个应用的目标非常明确:允许用户创建、编辑和删除待办事项,支持按状态过滤,并将数据保存在本地,确保刷新页面后不会丢失。我们将从需求分析开始,逐步完成技术选型、代码实现和部署上线,并在最后提供一个练习,帮助您进一步巩固所学内容。
通过这个项目,您将体验到:
- 组件化思维:如何将复杂的界面拆分为可重用的模块。
- 状态管理:如何在应用中高效地共享和更新数据。
- 路由设计:如何实现多页面导航。
- 数据持久化:如何使用本地存储保存用户数据。
准备好了吗?让我们开始吧!
需求分析
在动手写代码之前,我们需要明确这个待办事项应用的具体功能需求。一个清晰的需求清单不仅能指导开发过程,还能帮助我们理解每个功能的意义。以下是我们项目的核心需求:
- 创建待办事项
用户可以输入任务描述并添加到待办列表中。 - 编辑待办事项
用户可以修改已有任务的内容。 - 删除待办事项
用户可以移除不再需要的任务。 - 过滤待办事项
用户可以根据任务状态(全部、已完成、未完成)筛选列表。 - 数据持久化
数据将保存在浏览器本地存储中,刷新页面后依然可用。
为什么选择这些功能?
这些功能覆盖了待办事项应用的核心场景,同时也为学习 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
- 注册 Netlify :访问 Netlify 官网 并创建账号。
- 新建站点:在控制台选择"New site from Git"。
- 连接仓库:将项目推送至 GitHub 并连接。
- 配置构建 :
- 构建命令:
npm run build
- 发布目录:
dist
- 构建命令:
- 部署:点击"Deploy site",等待部署完成。
部署成功后,您将获得一个唯一的 URL,可以通过它访问您的待办事项应用。
练习:添加分类功能
为了帮助您巩固所学,我们设计了一个练习:为待办事项添加分类功能。
需求
- 用户可以为任务指定分类(如"工作"、"个人"、"学习")。
- 用户可以按分类过滤任务。
实现步骤
- 扩展数据结构
在todos
中为每个任务添加category
字段。 - 更新 TodoForm
添加分类选择下拉菜单。 - 更新过滤逻辑
在TodoList
和FilterButtons
中支持分类过滤。
示例代码
修改 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 项目的开发流程。您学习了组件拆分、路由设计、状态管理和数据持久化等核心技能,这些知识将成为您未来开发更复杂应用的基础。