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)

功能需求

  • 首页
  • 产品页面
  • 用户中心
  • 404 页面
  • 路由守卫

项目结构

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. 测试策略

  • 单元测试
  • 集成测试
  • E2E 测试

5. 部署和构建

  • 环境配置
  • 构建优化
  • CI/CD 流程

标签

React 项目实战 TodoApp 博客系统 购物车 多页面应用 企业级应用