技术文档中心
首页
React
Vue
TypeScript
Kotlin
React Native
Electron
Android
首页
React
Vue
TypeScript
Kotlin
React Native
Electron
Android
  • 入门基础

    • React 学习指南
    • React 快速入门
    • 状态管理基础
    • Hooks 基础
    • 组件通信
    • 生命周期与副作用
    • 实战项目
  • 进阶提升

    • Hooks 进阶
    • 组件设计模式
    • 性能优化
    • React Router
    • 表单处理
    • HTTP 请求
  • 状态管理

    • Context API
    • Redux 状态管理
    • Zustand 轻量状态管理
  • 高级主题

    • React + TypeScript
    • React 测试
    • 服务端渲染 (SSR)
    • 微前端架构

实战项目

项目 1: 计数器应用

目录结构

src/
├── App.jsx
├── main.jsx
├── Counter.jsx
└── Counter.css

功能需求

  • 显示当前计数
  • 增加/减少按钮
  • 重置按钮
  • 步长设置

实现代码

import { useState } from 'react';
import './Counter.css';

function Counter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);

  return (
    <div className="counter">
      <h1>计数器</h1>
      <div className="display">{count}</div>
      
      <div className="controls">
        <button onClick={() => setCount(count - step)}>-{step}</button>
        <button onClick={() => setCount(count + step)}>+{step}</button>
        <button onClick={() => setCount(0)}>重置</button>
      </div>
      
      <div className="step-control">
        <label>步长: </label>
        <input 
          type="number" 
          value={step}
          onChange={(e) => setStep(Number(e.target.value))}
          min="1"
        />
      </div>
    </div>
  );
}

export default Counter;

样式文件 Counter.css

.counter {
  max-width: 400px;
  margin: 50px auto;
  padding: 30px;
  border: 2px solid #ddd;
  border-radius: 10px;
  text-align: center;
}

.display {
  font-size: 72px;
  font-weight: bold;
  color: #333;
  margin: 30px 0;
}

.controls button {
  margin: 5px;
  padding: 10px 20px;
  font-size: 18px;
  cursor: pointer;
  border: none;
  border-radius: 5px;
  background: #007bff;
  color: white;
}

.controls button:hover {
  background: #0056b3;
}

.step-control {
  margin-top: 20px;
}

.step-control input {
  width: 60px;
  padding: 5px;
  font-size: 16px;
}

项目 2: 待办事项

目录结构

src/
├── App.jsx
├── main.jsx
├── TodoApp.jsx
└── TodoApp.css

功能需求

  • 添加待办
  • 标记完成
  • 删除待办
  • 筛选(全部/未完成/已完成)
  • 显示统计

实现代码

import { useState } from 'react';
import './TodoApp.css';

function TodoApp() {
  const [todos, setTodos] = useState([]);
  const [input, setInput] = useState('');
  const [filter, setFilter] = useState('all'); // all, active, completed

  const addTodo = () => {
    if (input.trim()) {
      setTodos([
        ...todos,
        { id: Date.now(), text: input, completed: false }
      ]);
      setInput('');
    }
  };

  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 clearCompleted = () => {
    setTodos(todos.filter(todo => !todo.completed));
  };

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

  const stats = {
    total: todos.length,
    active: todos.filter(t => !t.completed).length,
    completed: todos.filter(t => t.completed).length
  };

  return (
    <div className="todo-app">
      <h1>待办事项</h1>
      
      <div className="input-section">
        <input
          value={input}
          onChange={(e) => setInput(e.target.value)}
          onKeyPress={(e) => e.key === 'Enter' && addTodo()}
          placeholder="输入待办事项..."
        />
        <button onClick={addTodo}>添加</button>
      </div>

      <div className="filters">
        <button 
          className={filter === 'all' ? 'active' : ''}
          onClick={() => setFilter('all')}
        >
          全部 ({stats.total})
        </button>
        <button 
          className={filter === 'active' ? 'active' : ''}
          onClick={() => setFilter('active')}
        >
          未完成 ({stats.active})
        </button>
        <button 
          className={filter === 'completed' ? 'active' : ''}
          onClick={() => setFilter('completed')}
        >
          已完成 ({stats.completed})
        </button>
      </div>

      <ul className="todo-list">
        {filteredTodos.map(todo => (
          <li key={todo.id} className={todo.completed ? 'completed' : ''}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo(todo.id)}
            />
            <span>{todo.text}</span>
            <button onClick={() => deleteTodo(todo.id)}>删除</button>
          </li>
        ))}
      </ul>

      {stats.completed > 0 && (
        <button className="clear-btn" onClick={clearCompleted}>
          清除已完成
        </button>
      )}
    </div>
  );
}

export default TodoApp;

样式文件 TodoApp.css

.todo-app {
  max-width: 600px;
  margin: 50px auto;
  padding: 20px;
}

.input-section {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.input-section input {
  flex: 1;
  padding: 10px;
  font-size: 16px;
  border: 2px solid #ddd;
  border-radius: 5px;
}

.input-section button {
  padding: 10px 20px;
  font-size: 16px;
  background: #28a745;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

.filters {
  display: flex;
  gap: 10px;
  margin-bottom: 20px;
}

.filters button {
  padding: 8px 16px;
  border: 1px solid #ddd;
  background: white;
  cursor: pointer;
  border-radius: 5px;
}

.filters button.active {
  background: #007bff;
  color: white;
  border-color: #007bff;
}

.todo-list {
  list-style: none;
  padding: 0;
}

.todo-list li {
  display: flex;
  align-items: center;
  gap: 10px;
  padding: 12px;
  border: 1px solid #ddd;
  margin-bottom: 8px;
  border-radius: 5px;
}

.todo-list li.completed span {
  text-decoration: line-through;
  color: #999;
}

.todo-list li span {
  flex: 1;
}

.todo-list li button {
  padding: 5px 10px;
  background: #dc3545;
  color: white;
  border: none;
  border-radius: 3px;
  cursor: pointer;
}

.clear-btn {
  width: 100%;
  padding: 10px;
  margin-top: 20px;
  background: #6c757d;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

项目 3: 表单验证

目录结构

src/
├── App.jsx
├── main.jsx
├── RegisterForm.jsx
└── RegisterForm.css

功能需求

  • 用户名、邮箱、密码输入
  • 实时验证
  • 错误提示
  • 提交处理

实现代码

import { useState } from 'react';
import './RegisterForm.css';

function RegisterForm() {
  const [form, setForm] = useState({
    username: '',
    email: '',
    password: '',
    confirmPassword: ''
  });

  const [errors, setErrors] = useState({});
  const [touched, setTouched] = useState({});

  const validate = (name, value) => {
    switch (name) {
      case 'username':
        if (!value) return '用户名不能为空';
        if (value.length < 3) return '用户名至少 3 个字符';
        return '';
      
      case 'email':
        if (!value) return '邮箱不能为空';
        if (!/\S+@\S+\.\S+/.test(value)) return '邮箱格式不正确';
        return '';
      
      case 'password':
        if (!value) return '密码不能为空';
        if (value.length < 6) return '密码至少 6 个字符';
        return '';
      
      case 'confirmPassword':
        if (!value) return '请确认密码';
        if (value !== form.password) return '两次密码不一致';
        return '';
      
      default:
        return '';
    }
  };

  const handleChange = (e) => {
    const { name, value } = e.target;
    setForm({ ...form, [name]: value });
    
    // 实时验证
    if (touched[name]) {
      const error = validate(name, value);
      setErrors({ ...errors, [name]: error });
    }
  };

  const handleBlur = (e) => {
    const { name, value } = e.target;
    setTouched({ ...touched, [name]: true });
    
    const error = validate(name, value);
    setErrors({ ...errors, [name]: error });
  };

  const handleSubmit = (e) => {
    e.preventDefault();
    
    // 验证所有字段
    const newErrors = {};
    Object.keys(form).forEach(key => {
      const error = validate(key, form[key]);
      if (error) newErrors[key] = error;
    });
    
    setErrors(newErrors);
    setTouched({
      username: true,
      email: true,
      password: true,
      confirmPassword: true
    });
    
    // 如果没有错误,提交表单
    if (Object.keys(newErrors).length === 0) {
      console.log('提交表单:', form);
      alert('注册成功!');
      // 重置表单
      setForm({
        username: '',
        email: '',
        password: '',
        confirmPassword: ''
      });
      setTouched({});
    }
  };

  return (
    <div className="register-form">
      <h1>用户注册</h1>
      
      <form onSubmit={handleSubmit}>
        <div className="form-group">
          <label>用户名</label>
          <input
            name="username"
            value={form.username}
            onChange={handleChange}
            onBlur={handleBlur}
            className={errors.username && touched.username ? 'error' : ''}
          />
          {errors.username && touched.username && (
            <span className="error-msg">{errors.username}</span>
          )}
        </div>

        <div className="form-group">
          <label>邮箱</label>
          <input
            name="email"
            type="email"
            value={form.email}
            onChange={handleChange}
            onBlur={handleBlur}
            className={errors.email && touched.email ? 'error' : ''}
          />
          {errors.email && touched.email && (
            <span className="error-msg">{errors.email}</span>
          )}
        </div>

        <div className="form-group">
          <label>密码</label>
          <input
            name="password"
            type="password"
            value={form.password}
            onChange={handleChange}
            onBlur={handleBlur}
            className={errors.password && touched.password ? 'error' : ''}
          />
          {errors.password && touched.password && (
            <span className="error-msg">{errors.password}</span>
          )}
        </div>

        <div className="form-group">
          <label>确认密码</label>
          <input
            name="confirmPassword"
            type="password"
            value={form.confirmPassword}
            onChange={handleChange}
            onBlur={handleBlur}
            className={errors.confirmPassword && touched.confirmPassword ? 'error' : ''}
          />
          {errors.confirmPassword && touched.confirmPassword && (
            <span className="error-msg">{errors.confirmPassword}</span>
          )}
        </div>

        <button type="submit">注册</button>
      </form>
    </div>
  );
}

export default RegisterForm;

样式文件 RegisterForm.css

.register-form {
  max-width: 400px;
  margin: 50px auto;
  padding: 30px;
  border: 1px solid #ddd;
  border-radius: 10px;
}

.form-group {
  margin-bottom: 20px;
}

.form-group label {
  display: block;
  margin-bottom: 5px;
  font-weight: bold;
}

.form-group input {
  width: 100%;
  padding: 10px;
  font-size: 16px;
  border: 2px solid #ddd;
  border-radius: 5px;
  box-sizing: border-box;
}

.form-group input.error {
  border-color: #dc3545;
}

.error-msg {
  display: block;
  color: #dc3545;
  font-size: 14px;
  margin-top: 5px;
}

button[type="submit"] {
  width: 100%;
  padding: 12px;
  font-size: 18px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

button[type="submit"]:hover {
  background: #0056b3;
}

项目 4: 天气查询

目录结构

src/
├── App.jsx
├── main.jsx
├── WeatherApp.jsx
└── WeatherApp.css

功能需求

  • 城市搜索
  • 显示天气信息
  • 加载状态
  • 错误处理

实现代码

import { useState } from 'react';
import './WeatherApp.css';

function WeatherApp() {
  const [city, setCity] = useState('');
  const [weather, setWeather] = useState(null);
  const [loading, setLoading] = useState(false);
  const [error, setError] = useState('');

  const fetchWeather = async () => {
    if (!city.trim()) {
      setError('请输入城市名称');
      return;
    }

    setLoading(true);
    setError('');
    
    try {
      // 模拟 API 调用
      await new Promise(resolve => setTimeout(resolve, 1000));
      
      // 模拟数据
      const mockData = {
        city: city,
        temperature: Math.floor(Math.random() * 30) + 10,
        condition: ['晴', '多云', '雨', '雪'][Math.floor(Math.random() * 4)],
        humidity: Math.floor(Math.random() * 50) + 30,
        wind: Math.floor(Math.random() * 20) + 5
      };
      
      setWeather(mockData);
    } catch (err) {
      setError('获取天气信息失败');
    } finally {
      setLoading(false);
    }
  };

  const handleKeyPress = (e) => {
    if (e.key === 'Enter') {
      fetchWeather();
    }
  };

  return (
    <div className="weather-app">
      <h1>天气查询</h1>
      
      <div className="search-box">
        <input
          value={city}
          onChange={(e) => setCity(e.target.value)}
          onKeyPress={handleKeyPress}
          placeholder="输入城市名称..."
        />
        <button onClick={fetchWeather} disabled={loading}>
          {loading ? '查询中...' : '查询'}
        </button>
      </div>

      {error && <div className="error">{error}</div>}

      {weather && !loading && (
        <div className="weather-info">
          <h2>{weather.city}</h2>
          <div className="temperature">{weather.temperature}°C</div>
          <div className="condition">{weather.condition}</div>
          <div className="details">
            <div>湿度: {weather.humidity}%</div>
            <div>风速: {weather.wind} km/h</div>
          </div>
        </div>
      )}
    </div>
  );
}

export default WeatherApp;

样式文件 WeatherApp.css

.weather-app {
  max-width: 500px;
  margin: 50px auto;
  padding: 30px;
  text-align: center;
}

.search-box {
  display: flex;
  gap: 10px;
  margin: 30px 0;
}

.search-box input {
  flex: 1;
  padding: 12px;
  font-size: 16px;
  border: 2px solid #ddd;
  border-radius: 5px;
}

.search-box button {
  padding: 12px 24px;
  font-size: 16px;
  background: #007bff;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

.search-box button:disabled {
  background: #ccc;
  cursor: not-allowed;
}

.error {
  color: #dc3545;
  padding: 10px;
  margin: 20px 0;
  background: #f8d7da;
  border-radius: 5px;
}

.weather-info {
  margin-top: 30px;
  padding: 30px;
  background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
  color: white;
  border-radius: 15px;
}

.temperature {
  font-size: 72px;
  font-weight: bold;
  margin: 20px 0;
}

.condition {
  font-size: 24px;
  margin-bottom: 20px;
}

.details {
  display: flex;
  justify-content: space-around;
  margin-top: 20px;
  padding-top: 20px;
  border-top: 1px solid rgba(255,255,255,0.3);
}

综合项目: 多功能应用

完整目录结构

my-project/
├── public/
├── src/
│   ├── components/
│   │   ├── Counter.jsx
│   │   ├── Counter.css
│   │   ├── TodoApp.jsx
│   │   ├── TodoApp.css
│   │   ├── RegisterForm.jsx
│   │   ├── RegisterForm.css
│   │   ├── WeatherApp.jsx
│   │   └── WeatherApp.css
│   ├── App.jsx
│   ├── App.css
│   └── main.jsx
├── index.html
├── package.json
└── vite.config.js

App.jsx (主文件)

import { useState } from 'react';
import Counter from './components/Counter';
import TodoApp from './components/TodoApp';
import RegisterForm from './components/RegisterForm';
import WeatherApp from './components/WeatherApp';
import './App.css';

function App() {
  const [activeTab, setActiveTab] = useState('counter');

  const tabs = [
    { id: 'counter', name: '计数器', component: <Counter /> },
    { id: 'todo', name: '待办事项', component: <TodoApp /> },
    { id: 'form', name: '表单验证', component: <RegisterForm /> },
    { id: 'weather', name: '天气查询', component: <WeatherApp /> }
  ];

  return (
    <div className="app">
      <nav className="tabs">
        {tabs.map(tab => (
          <button
            key={tab.id}
            className={activeTab === tab.id ? 'active' : ''}
            onClick={() => setActiveTab(tab.id)}
          >
            {tab.name}
          </button>
        ))}
      </nav>
      
      <div className="content">
        {tabs.find(tab => tab.id === activeTab)?.component}
      </div>
    </div>
  );
}

export default App;

App.css

.app {
  min-height: 100vh;
  background: #f5f5f5;
}

.tabs {
  display: flex;
  gap: 10px;
  padding: 20px;
  background: white;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}

.tabs button {
  padding: 10px 20px;
  border: none;
  background: #f0f0f0;
  cursor: pointer;
  border-radius: 5px;
  font-size: 16px;
}

.tabs button.active {
  background: #007bff;
  color: white;
}

.content {
  padding: 20px;
}

快速开始

# 创建项目
yarn create vite my-project --template react
cd my-project

# 安装依赖
yarn

# 创建 components 目录
mkdir src/components

# 将各组件代码复制到 src/components/ 目录

# 启动开发服务器
yarn dev
最近更新: 2026/1/27 15:51
Contributors: hailong
Prev
生命周期与副作用