React 项目实战
项目一:待办事项应用 (Todo App)
功能需求
- 添加新任务
- 标记任务完成/未完成
- 删除任务
- 编辑任务
- 任务筛选(全部/已完成/未完成)
- 本地存储
项目结构
src/
├── components/
│ ├── TodoApp.jsx
│ ├── TodoForm.jsx
│ ├── TodoList.jsx
│ ├── TodoItem.jsx
│ └── FilterButtons.jsx
├── hooks/
│ └── useLocalStorage.js
├── styles/
│ └── App.css
└── App.js
核心代码实现
1. 自定义 Hook - 本地存储
// hooks/useLocalStorage.js
import { useState, useEffect } from 'react';
function useLocalStorage(key, initialValue) {
const [storedValue, setStoredValue] = useState(() => {
try {
const item = window.localStorage.getItem(key);
return item ? JSON.parse(item) : initialValue;
} catch (error) {
console.error(error);
return initialValue;
}
});
const setValue = (value) => {
try {
setStoredValue(value);
window.localStorage.setItem(key, JSON.stringify(value));
} catch (error) {
console.error(error);
}
};
return [storedValue, setValue];
}
export default useLocalStorage;
2. 主应用组件
// components/TodoApp.jsx
import React, { useState } from 'react';
import TodoForm from './TodoForm';
import TodoList from './TodoList';
import FilterButtons from './FilterButtons';
import useLocalStorage from '../hooks/useLocalStorage';
import './App.css';
function TodoApp() {
const [todos, setTodos] = useLocalStorage('todos', []);
const [filter, setFilter] = useState('all');
const addTodo = (text) => {
const newTodo = {
id: Date.now(),
text,
completed: false,
createdAt: new Date().toISOString()
};
setTodos([...todos, newTodo]);
};
const toggleTodo = (id) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
));
};
const deleteTodo = (id) => {
setTodos(todos.filter(todo => todo.id !== id));
};
const editTodo = (id, newText) => {
setTodos(todos.map(todo =>
todo.id === id ? { ...todo, text: newText } : todo
));
};
const filteredTodos = todos.filter(todo => {
if (filter === 'completed') return todo.completed;
if (filter === 'active') return !todo.completed;
return true;
});
const completedCount = todos.filter(todo => todo.completed).length;
const totalCount = todos.length;
return (
<div className="todo-app">
<h1>待办事项</h1>
<TodoForm onAddTodo={addTodo} />
<FilterButtons
filter={filter}
onFilterChange={setFilter}
completedCount={completedCount}
totalCount={totalCount}
/>
<TodoList
todos={filteredTodos}
onToggleTodo={toggleTodo}
onDeleteTodo={deleteTodo}
onEditTodo={editTodo}
/>
</div>
);
}
export default TodoApp;
3. 添加任务表单
// components/TodoForm.jsx
import React, { useState } from 'react';
function TodoForm({ onAddTodo }) {
const [text, setText] = useState('');
const handleSubmit = (e) => {
e.preventDefault();
if (text.trim()) {
onAddTodo(text.trim());
setText('');
}
};
return (
<form onSubmit={handleSubmit} className="todo-form">
<input
type="text"
value={text}
onChange={(e) => setText(e.target.value)}
placeholder="添加新任务..."
className="todo-input"
/>
<button type="submit" className="add-button">
添加
</button>
</form>
);
}
export default TodoForm;
4. 任务列表
// components/TodoList.jsx
import React from 'react';
import TodoItem from './TodoItem';
function TodoList({ todos, onToggleTodo, onDeleteTodo, onEditTodo }) {
if (todos.length === 0) {
return <div className="empty-message">暂无任务</div>;
}
return (
<ul className="todo-list">
{todos.map(todo => (
<TodoItem
key={todo.id}
todo={todo}
onToggle={onToggleTodo}
onDelete={onDeleteTodo}
onEdit={onEditTodo}
/>
))}
</ul>
);
}
export default TodoList;
5. 任务项组件
// components/TodoItem.jsx
import React, { useState } from 'react';
function TodoItem({ todo, onToggle, onDelete, onEdit }) {
const [isEditing, setIsEditing] = useState(false);
const [editText, setEditText] = useState(todo.text);
const handleEdit = () => {
if (isEditing) {
if (editText.trim()) {
onEdit(todo.id, editText.trim());
}
setIsEditing(false);
} else {
setIsEditing(true);
}
};
const handleKeyPress = (e) => {
if (e.key === 'Enter') {
handleEdit();
} else if (e.key === 'Escape') {
setEditText(todo.text);
setIsEditing(false);
}
};
return (
<li className={`todo-item ${todo.completed ? 'completed' : ''}`}>
<input
type="checkbox"
checked={todo.completed}
onChange={() => onToggle(todo.id)}
className="todo-checkbox"
/>
{isEditing ? (
<input
type="text"
value={editText}
onChange={(e) => setEditText(e.target.value)}
onKeyDown={handleKeyPress}
onBlur={handleEdit}
className="edit-input"
autoFocus
/>
) : (
<span className="todo-text">{todo.text}</span>
)}
<div className="todo-actions">
<button onClick={handleEdit} className="edit-button">
{isEditing ? '保存' : '编辑'}
</button>
<button onClick={() => onDelete(todo.id)} className="delete-button">
删除
</button>
</div>
</li>
);
}
export default TodoItem;
6. 筛选按钮
// components/FilterButtons.jsx
import React from 'react';
function FilterButtons({ filter, onFilterChange, completedCount, totalCount }) {
const filters = [
{ key: 'all', label: '全部' },
{ key: 'active', label: '未完成' },
{ key: 'completed', label: '已完成' }
];
return (
<div className="filter-buttons">
{filters.map(({ key, label }) => (
<button
key={key}
className={`filter-button ${filter === key ? 'active' : ''}`}
onClick={() => onFilterChange(key)}
>
{label}
</button>
))}
<div className="todo-stats">
已完成: {completedCount} / {totalCount}
</div>
</div>
);
}
export default FilterButtons;
项目二:个人博客系统
功能需求
- 文章列表展示
- 文章详情页面
- 文章搜索功能
- 文章分类筛选
- 评论系统
- 响应式设计
项目结构
src/
├── components/
│ ├── BlogApp.jsx
│ ├── Header.jsx
│ ├── ArticleList.jsx
│ ├── ArticleCard.jsx
│ ├── ArticleDetail.jsx
│ ├── SearchBar.jsx
│ ├── CategoryFilter.jsx
│ ├── CommentForm.jsx
│ └── CommentList.jsx
├── hooks/
│ ├── useFetch.js
│ └── useDebounce.js
├── data/
│ └── mockData.js
└── styles/
└── Blog.css
核心代码实现
1. 数据获取 Hook
// hooks/useFetch.js
import { useState, useEffect } from 'react';
function useFetch(url) {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
let cancelled = false;
const fetchData = async () => {
try {
setLoading(true);
setError(null);
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const result = await response.json();
if (!cancelled) {
setData(result);
}
} catch (err) {
if (!cancelled) {
setError(err);
}
} finally {
if (!cancelled) {
setLoading(false);
}
}
};
fetchData();
return () => {
cancelled = true;
};
}, [url]);
return { data, loading, error };
}
export default useFetch;
2. 防抖 Hook
// hooks/useDebounce.js
import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
}, [value, delay]);
return debouncedValue;
}
export default useDebounce;
3. 主博客应用
// components/BlogApp.jsx
import React, { useState, useMemo } from 'react';
import Header from './Header';
import ArticleList from './ArticleList';
import ArticleDetail from './ArticleDetail';
import SearchBar from './SearchBar';
import CategoryFilter from './CategoryFilter';
import useFetch from '../hooks/useFetch';
import useDebounce from '../hooks/useDebounce';
import './Blog.css';
function BlogApp() {
const [selectedArticle, setSelectedArticle] = useState(null);
const [searchTerm, setSearchTerm] = useState('');
const [selectedCategory, setSelectedCategory] = useState('all');
const debouncedSearchTerm = useDebounce(searchTerm, 300);
const { data: articles, loading, error } = useFetch('/api/articles');
const filteredArticles = useMemo(() => {
if (!articles) return [];
return articles.filter(article => {
const matchesSearch = article.title
.toLowerCase()
.includes(debouncedSearchTerm.toLowerCase()) ||
article.content
.toLowerCase()
.includes(debouncedSearchTerm.toLowerCase());
const matchesCategory = selectedCategory === 'all' ||
article.category === selectedCategory;
return matchesSearch && matchesCategory;
});
}, [articles, debouncedSearchTerm, selectedCategory]);
const categories = useMemo(() => {
if (!articles) return [];
return [...new Set(articles.map(article => article.category))];
}, [articles]);
if (loading) return <div className="loading">加载中...</div>;
if (error) return <div className="error">加载失败: {error.message}</div>;
return (
<div className="blog-app">
<Header />
{selectedArticle ? (
<ArticleDetail
article={selectedArticle}
onBack={() => setSelectedArticle(null)}
/>
) : (
<>
<div className="filters">
<SearchBar
searchTerm={searchTerm}
onSearchChange={setSearchTerm}
/>
<CategoryFilter
categories={categories}
selectedCategory={selectedCategory}
onCategoryChange={setSelectedCategory}
/>
</div>
<ArticleList
articles={filteredArticles}
onArticleSelect={setSelectedArticle}
/>
</>
)}
</div>
);
}
export default BlogApp;
4. 文章列表
// components/ArticleList.jsx
import React from 'react';
import ArticleCard from './ArticleCard';
function ArticleList({ articles, onArticleSelect }) {
if (articles.length === 0) {
return <div className="no-articles">没有找到相关文章</div>;
}
return (
<div className="article-list">
{articles.map(article => (
<ArticleCard
key={article.id}
article={article}
onClick={() => onArticleSelect(article)}
/>
))}
</div>
);
}
export default ArticleList;
5. 文章卡片
// components/ArticleCard.jsx
import React from 'react';
function ArticleCard({ article, onClick }) {
const formatDate = (dateString) => {
return new Date(dateString).toLocaleDateString('zh-CN');
};
const truncateContent = (content, maxLength = 150) => {
if (content.length <= maxLength) return content;
return content.substring(0, maxLength) + '...';
};
return (
<div className="article-card" onClick={onClick}>
<div className="article-meta">
<span className="article-category">{article.category}</span>
<span className="article-date">{formatDate(article.createdAt)}</span>
</div>
<h2 className="article-title">{article.title}</h2>
<p className="article-excerpt">
{truncateContent(article.content)}
</p>
<div className="article-footer">
<span className="article-author">作者: {article.author}</span>
<span className="article-views">阅读: {article.views}</span>
</div>
</div>
);
}
export default ArticleCard;
项目三:电商购物车
功能需求
- 商品展示
- 购物车管理
- 用户认证
- 订单处理
- 状态管理
项目结构
src/
├── components/
│ ├── EcommerceApp.jsx
│ ├── ProductList.jsx
│ ├── ProductCard.jsx
│ ├── Cart.jsx
│ ├── CartItem.jsx
│ ├── Checkout.jsx
│ └── Auth.jsx
├── context/
│ ├── AuthContext.jsx
│ └── CartContext.jsx
├── hooks/
│ └── useAuth.js
└── styles/
└── Ecommerce.css
核心代码实现
1. 购物车上下文
// context/CartContext.jsx
import React, { createContext, useContext, useReducer } from 'react';
const CartContext = createContext();
const cartReducer = (state, action) => {
switch (action.type) {
case 'ADD_TO_CART':
const existingItem = state.items.find(item => item.id === action.payload.id);
if (existingItem) {
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: item.quantity + 1 }
: item
)
};
}
return {
...state,
items: [...state.items, { ...action.payload, quantity: 1 }]
};
case 'REMOVE_FROM_CART':
return {
...state,
items: state.items.filter(item => item.id !== action.payload)
};
case 'UPDATE_QUANTITY':
return {
...state,
items: state.items.map(item =>
item.id === action.payload.id
? { ...item, quantity: action.payload.quantity }
: item
).filter(item => item.quantity > 0)
};
case 'CLEAR_CART':
return {
...state,
items: []
};
default:
return state;
}
};
export function CartProvider({ children }) {
const [state, dispatch] = useReducer(cartReducer, { items: [] });
const addToCart = (product) => {
dispatch({ type: 'ADD_TO_CART', payload: product });
};
const removeFromCart = (productId) => {
dispatch({ type: 'REMOVE_FROM_CART', payload: productId });
};
const updateQuantity = (productId, quantity) => {
dispatch({ type: 'UPDATE_QUANTITY', payload: { id: productId, quantity } });
};
const clearCart = () => {
dispatch({ type: 'CLEAR_CART' });
};
const getTotalPrice = () => {
return state.items.reduce((total, item) => total + (item.price * item.quantity), 0);
};
const getTotalItems = () => {
return state.items.reduce((total, item) => total + item.quantity, 0);
};
const value = {
...state,
addToCart,
removeFromCart,
updateQuantity,
clearCart,
getTotalPrice,
getTotalItems
};
return (
<CartContext.Provider value={value}>
{children}
</CartContext.Provider>
);
}
export function useCart() {
const context = useContext(CartContext);
if (!context) {
throw new Error('useCart must be used within a CartProvider');
}
return context;
}
2. 购物车组件
// components/Cart.jsx
import React, { useState } from 'react';
import { useCart } from '../context/CartContext';
import CartItem from './CartItem';
import Checkout from './Checkout';
function Cart({ isOpen, onClose }) {
const { items, getTotalPrice, getTotalItems, clearCart } = useCart();
const [showCheckout, setShowCheckout] = useState(false);
if (!isOpen) return null;
const handleCheckout = () => {
setShowCheckout(true);
};
const handleCheckoutComplete = () => {
clearCart();
setShowCheckout(false);
onClose();
};
if (showCheckout) {
return (
<Checkout
onBack={() => setShowCheckout(false)}
onComplete={handleCheckoutComplete}
/>
);
}
return (
<div className="cart-overlay">
<div className="cart">
<div className="cart-header">
<h2>购物车 ({getTotalItems()})</h2>
<button onClick={onClose} className="close-button">×</button>
</div>
<div className="cart-content">
{items.length === 0 ? (
<div className="empty-cart">购物车是空的</div>
) : (
<>
<div className="cart-items">
{items.map(item => (
<CartItem key={item.id} item={item} />
))}
</div>
<div className="cart-footer">
<div className="cart-total">
总计: ¥{getTotalPrice().toFixed(2)}
</div>
<div className="cart-actions">
<button onClick={clearCart} className="clear-button">
清空购物车
</button>
<button onClick={handleCheckout} className="checkout-button">
结算
</button>
</div>
</div>
</>
)}
</div>
</div>
</div>
);
}
export default Cart;
项目四:多页面应用 (React Router)
功能需求
项目结构
src/
├── components/
│ ├── App.jsx
│ ├── Layout.jsx
│ ├── Navbar.jsx
│ └── Footer.jsx
├── pages/
│ ├── Home.jsx
│ ├── Products.jsx
│ ├── ProductDetail.jsx
│ ├── Profile.jsx
│ └── NotFound.jsx
├── context/
│ └── AuthContext.jsx
├── components/
│ └── ProtectedRoute.jsx
└── styles/
└── App.css
核心代码实现
1. 路由配置
// App.jsx
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import Layout from './components/Layout';
import Home from './pages/Home';
import Products from './pages/Products';
import ProductDetail from './pages/ProductDetail';
import Profile from './pages/Profile';
import NotFound from './pages/NotFound';
import ProtectedRoute from './components/ProtectedRoute';
import { AuthProvider } from './context/AuthContext';
function App() {
return (
<AuthProvider>
<Router>
<Layout>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/products" element={<Products />} />
<Route path="/products/:id" element={<ProductDetail />} />
<Route
path="/profile"
element={
<ProtectedRoute>
<Profile />
</ProtectedRoute>
}
/>
<Route path="*" element={<NotFound />} />
</Routes>
</Layout>
</Router>
</AuthProvider>
);
}
export default App;
2. 受保护路由
// components/ProtectedRoute.jsx
import React from 'react';
import { Navigate } from 'react-router-dom';
import { useAuth } from '../context/AuthContext';
function ProtectedRoute({ children }) {
const { isAuthenticated, loading } = useAuth();
if (loading) {
return <div className="loading">加载中...</div>;
}
return isAuthenticated ? children : <Navigate to="/login" replace />;
}
export default ProtectedRoute;
项目五:企业级应用
功能需求
技术栈
- React 18
- TypeScript
- Redux Toolkit
- React Query
- Material-UI
- Socket.io
- Chart.js
项目结构
src/
├── components/
│ ├── Dashboard/
│ ├── Users/
│ ├── Charts/
│ └── Chat/
├── pages/
├── store/
├── services/
├── hooks/
├── utils/
└── types/
开发最佳实践
1. 组件设计原则
2. 状态管理策略
- 本地状态 vs 全局状态
- Context API vs Redux
- 状态提升
3. 性能优化
- React.memo
- useMemo 和 useCallback
- 代码分割
- 懒加载
4. 测试策略
5. 部署和构建
标签
React 项目实战 TodoApp 博客系统 购物车 多页面应用 企业级应用