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 項目:
npm create vite@latest todo-app -- --template react
cd todo-app
npm install
npm run dev
安裝必要的依賴:
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
中配置路由:
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
中添加導航到添加頁面的按鈕:
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
中:
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
中訪問和過濾數據:
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
中實現單個待辦事項的展示和操作:
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"><inputtype="checkbox"checked={todo.completed}onChange={toggleComplete}className="mr-2"/><span className={todo.completed ? 'line-through text-gray-500' : ''}>{todo.text}</span></div><div><Linkto={`/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
中實現添加和編輯表單:
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}><inputtype="text"value={text}onChange={(e) => setText(e.target.value)}className="w-full p-2 border rounded mb-4"placeholder="請輸入任務描述"/><buttontype="submit"className="bg-blue-500 text-white px-4 py-2 rounded">保存</button></form></div>);
}export default TodoForm;
FilterButtons
在 FilterButtons.jsx
中實現過濾按鈕:
import { useContext } from 'react';
import { TodoContext } from '../context/TodoContext';function FilterButtons() {const { filter, setFilter } = useContext(TodoContext);return (<div className="mb-4"><buttononClick={() => setFilter('all')}className={`mr-2 px-4 py-2 rounded ${filter === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}>全部</button><buttononClick={() => setFilter('completed')}className={`mr-2 px-4 py-2 rounded ${filter === 'completed' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}>已完成</button><buttononClick={() => 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. 構建項目
運行以下命令生成靜態文件:
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
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
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}><inputtype="text"value={text}onChange={(e) => setText(e.target.value)}className="w-full p-2 border rounded mb-4"placeholder="請輸入任務描述"/><selectvalue={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><buttontype="submit"className="bg-blue-500 text-white px-4 py-2 rounded">保存</button></form></div>);
}
修改 TodoList 和 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;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"><buttononClick={() => setFilter('all')}className={`px-4 py-2 rounded ${filter === 'all' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}>全部</button><buttononClick={() => setFilter('completed')}className={`px-4 py-2 rounded ${filter === 'completed' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}>已完成</button><buttononClick={() => setFilter('incomplete')}className={`px-4 py-2 rounded ${filter === 'incomplete' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}>未完成</button><buttononClick={() => setFilter('工作')}className={`px-4 py-2 rounded ${filter === '工作' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}>工作</button><buttononClick={() => setFilter('個人')}className={`px-4 py-2 rounded ${filter === '個人' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}>個人</button><buttononClick={() => setFilter('學習')}className={`px-4 py-2 rounded ${filter === '學習' ? 'bg-blue-500 text-white' : 'bg-gray-200'}`}>學習</button></div>);
}
練習目標
通過這個練習,您將學會如何擴展現有功能,提升對狀態管理和組件通信的理解。
注意事項
- 初學者友好:本文避免了復雜的概念,所有代碼都盡量保持簡單直觀。
- 學習建議:建議您邊閱讀邊動手實現,遇到問題時查閱 React 官方文檔和Vite 文檔。
- 擴展思路:完成項目后,可以嘗試添加更多功能,如任務優先級、截止日期或提醒功能。
結語
通過這個在線待辦事項應用項目,您從需求分析到部署上線,完整地走過了一個 React 項目的開發流程。您學習了組件拆分、路由設計、狀態管理和數據持久化等核心技能,這些知識將成為您未來開發更復雜應用的基礎。