Skip to content

计数器应用

学习目标

通过本章实战项目,你将全面掌握:

  • useState的实战应用
  • 事件处理的完整流程
  • 组件设计与拆分
  • State更新策略
  • 功能扩展与优化
  • 性能优化技巧
  • 组件测试方法
  • React 19新特性应用

项目概述

计数器应用是学习React的经典入门项目。虽然简单,但涵盖了React的核心概念:

  • State管理
  • 事件处理
  • 组件组合
  • 条件渲染
  • 性能优化

我们将从基础版本逐步迭代到高级版本,学习React开发的完整流程。

第一部分:基础计数器

1.1 最简单的计数器

jsx
import { useState } from 'react';

function BasicCounter() {
  const [count, setCount] = useState(0);
  
  return (
    <div>
      <h1>计数器</h1>
      <p>当前值:{count}</p>
      <button onClick={() => setCount(count + 1)}>+1</button>
      <button onClick={() => setCount(count - 1)}>-1</button>
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  );
}

export default BasicCounter;

代码解析

jsx
// 1. 导入必要的Hook
import { useState } from 'react';

// 2. 定义函数组件
function BasicCounter() {
  // 3. 使用useState创建State
  // count: 当前计数值
  // setCount: 更新count的函数
  // 0: 初始值
  const [count, setCount] = useState(0);
  
  // 4. 渲染UI
  return (
    <div>
      {/* 显示当前计数 */}
      <p>当前值:{count}</p>
      
      {/* 增加按钮:使用箭头函数 */}
      <button onClick={() => setCount(count + 1)}>+1</button>
      
      {/* 减少按钮 */}
      <button onClick={() => setCount(count - 1)}>-1</button>
      
      {/* 重置按钮:设置为0 */}
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  );
}

1.2 添加样式

jsx
function StyledCounter() {
  const [count, setCount] = useState(0);
  
  return (
    <div style={{
      maxWidth: '400px',
      margin: '50px auto',
      padding: '30px',
      textAlign: 'center',
      borderRadius: '10px',
      boxShadow: '0 2px 10px rgba(0,0,0,0.1)',
      background: 'white'
    }}>
      <h1 style={{
        margin: '0 0 20px',
        color: '#333',
        fontSize: '28px'
      }}>
        计数器应用
      </h1>
      
      <p style={{
        fontSize: '48px',
        margin: '30px 0',
        color: count > 0 ? '#28a745' : count < 0 ? '#dc3545' : '#333',
        fontWeight: 'bold'
      }}>
        {count}
      </p>
      
      <div style={{
        display: 'flex',
        gap: '10px',
        justifyContent: 'center'
      }}>
        <button
          onClick={() => setCount(count - 1)}
          style={{
            padding: '12px 24px',
            fontSize: '18px',
            border: 'none',
            borderRadius: '5px',
            background: '#dc3545',
            color: 'white',
            cursor: 'pointer'
          }}
        >
          -1
        </button>
        
        <button
          onClick={() => setCount(0)}
          style={{
            padding: '12px 24px',
            fontSize: '18px',
            border: 'none',
            borderRadius: '5px',
            background: '#6c757d',
            color: 'white',
            cursor: 'pointer'
          }}
        >
          重置
        </button>
        
        <button
          onClick={() => setCount(count + 1)}
          style={{
            padding: '12px 24px',
            fontSize: '18px',
            border: 'none',
            borderRadius: '5px',
            background: '#28a745',
            color: 'white',
            cursor: 'pointer'
          }}
        >
          +1
        </button>
      </div>
    </div>
  );
}

1.3 使用CSS类名

jsx
// Counter.jsx
function Counter() {
  const [count, setCount] = useState(0);
  
  return (
    <div className="counter-container">
      <h1 className="counter-title">计数器应用</h1>
      
      <p className={`counter-display ${count > 0 ? 'positive' : count < 0 ? 'negative' : ''}`}>
        {count}
      </p>
      
      <div className="counter-buttons">
        <button className="btn btn-danger" onClick={() => setCount(count - 1)}>
          -1
        </button>
        <button className="btn btn-secondary" onClick={() => setCount(0)}>
          重置
        </button>
        <button className="btn btn-success" onClick={() => setCount(count + 1)}>
          +1
        </button>
      </div>
    </div>
  );
}
css
/* Counter.css */
.counter-container {
  max-width: 400px;
  margin: 50px auto;
  padding: 30px;
  text-align: center;
  border-radius: 10px;
  box-shadow: 0 2px 10px rgba(0, 0, 0, 0.1);
  background: white;
}

.counter-title {
  margin: 0 0 20px;
  color: #333;
  font-size: 28px;
}

.counter-display {
  font-size: 48px;
  margin: 30px 0;
  color: #333;
  font-weight: bold;
  transition: color 0.3s;
}

.counter-display.positive {
  color: #28a745;
}

.counter-display.negative {
  color: #dc3545;
}

.counter-buttons {
  display: flex;
  gap: 10px;
  justify-content: center;
}

.btn {
  padding: 12px 24px;
  fontSize: 18px;
  border: none;
  border-radius: 5px;
  color: white;
  cursor: pointer;
  transition: opacity 0.2s;
}

.btn:hover {
  opacity: 0.9;
}

.btn:active {
  transform: translateY(1px);
}

.btn-danger {
  background: #dc3545;
}

.btn-secondary {
  background: #6c757d;
}

.btn-success {
  background: #28a745;
}

第二部分:增强功能

2.1 添加步长控制

jsx
function StepCounter() {
  const [count, setCount] = useState(0);
  const [step, setStep] = useState(1);
  
  const increment = () => {
    setCount(count + step);
  };
  
  const decrement = () => {
    setCount(count - step);
  };
  
  const reset = () => {
    setCount(0);
  };
  
  return (
    <div className="counter-container">
      <h1>步长计数器</h1>
      
      <p className="counter-display">{count}</p>
      
      <div className="step-control">
        <label>步长:</label>
        <input
          type="number"
          value={step}
          onChange={(e) => setStep(Number(e.target.value))}
          min="1"
          max="100"
        />
      </div>
      
      <div className="counter-buttons">
        <button onClick={decrement}>-{step}</button>
        <button onClick={reset}>重置</button>
        <button onClick={increment}>+{step}</button>
      </div>
    </div>
  );
}

2.2 添加范围限制

jsx
function RangeCounter() {
  const [count, setCount] = useState(0);
  const [min, setMin] = useState(-10);
  const [max, setMax] = useState(10);
  
  const increment = () => {
    setCount(prev => Math.min(prev + 1, max));
  };
  
  const decrement = () => {
    setCount(prev => Math.max(prev - 1, min));
  };
  
  const reset = () => {
    setCount(0);
  };
  
  return (
    <div className="counter-container">
      <h1>范围计数器</h1>
      
      <p className="counter-display">{count}</p>
      
      <div className="range-controls">
        <div>
          <label>最小值:</label>
          <input
            type="number"
            value={min}
            onChange={(e) => setMin(Number(e.target.value))}
          />
        </div>
        
        <div>
          <label>最大值:</label>
          <input
            type="number"
            value={max}
            onChange={(e) => setMax(Number(e.target.value))}
          />
        </div>
      </div>
      
      <div className="counter-buttons">
        <button onClick={decrement} disabled={count <= min}>
          -1
        </button>
        <button onClick={reset}>重置</button>
        <button onClick={increment} disabled={count >= max}>
          +1
        </button>
      </div>
      
      <div className="progress-bar">
        <div
          className="progress-fill"
          style={{
            width: `${((count - min) / (max - min)) * 100}%`
          }}
        />
      </div>
    </div>
  );
}

2.3 添加历史记录

jsx
function HistoryCounter() {
  const [count, setCount] = useState(0);
  const [history, setHistory] = useState([0]);
  const [historyIndex, setHistoryIndex] = useState(0);
  
  const addToHistory = (newCount) => {
    const newHistory = history.slice(0, historyIndex + 1);
    newHistory.push(newCount);
    setHistory(newHistory);
    setHistoryIndex(newHistory.length - 1);
    setCount(newCount);
  };
  
  const increment = () => {
    addToHistory(count + 1);
  };
  
  const decrement = () => {
    addToHistory(count - 1);
  };
  
  const reset = () => {
    addToHistory(0);
  };
  
  const undo = () => {
    if (historyIndex > 0) {
      const newIndex = historyIndex - 1;
      setHistoryIndex(newIndex);
      setCount(history[newIndex]);
    }
  };
  
  const redo = () => {
    if (historyIndex < history.length - 1) {
      const newIndex = historyIndex + 1;
      setHistoryIndex(newIndex);
      setCount(history[newIndex]);
    }
  };
  
  return (
    <div className="counter-container">
      <h1>历史计数器</h1>
      
      <p className="counter-display">{count}</p>
      
      <div className="counter-buttons">
        <button onClick={decrement}>-1</button>
        <button onClick={reset}>重置</button>
        <button onClick={increment}>+1</button>
      </div>
      
      <div className="history-controls">
        <button onClick={undo} disabled={historyIndex === 0}>
          ↶ 撤销
        </button>
        <button onClick={redo} disabled={historyIndex === history.length - 1}>
          ↷ 重做
        </button>
      </div>
      
      <div className="history-list">
        <h3>历史记录:</h3>
        <ul>
          {history.map((value, index) => (
            <li
              key={index}
              className={index === historyIndex ? 'current' : ''}
              onClick={() => {
                setHistoryIndex(index);
                setCount(value);
              }}
            >
              {value} {index === historyIndex && '← 当前'}
            </li>
          ))}
        </ul>
      </div>
    </div>
  );
}

2.4 多个计数器

jsx
function MultiCounter() {
  const [counters, setCounters] = useState([
    { id: 1, value: 0, name: '计数器1' },
    { id: 2, value: 0, name: '计数器2' },
    { id: 3, value: 0, name: '计数器3' }
  ]);
  
  const updateCounter = (id, delta) => {
    setCounters(counters.map(counter =>
      counter.id === id
        ? { ...counter, value: counter.value + delta }
        : counter
    ));
  };
  
  const resetCounter = (id) => {
    setCounters(counters.map(counter =>
      counter.id === id
        ? { ...counter, value: 0 }
        : counter
    ));
  };
  
  const addCounter = () => {
    const newId = Math.max(...counters.map(c => c.id)) + 1;
    setCounters([
      ...counters,
      { id: newId, value: 0, name: `计数器${newId}` }
    ]);
  };
  
  const removeCounter = (id) => {
    setCounters(counters.filter(counter => counter.id !== id));
  };
  
  const resetAll = () => {
    setCounters(counters.map(counter => ({ ...counter, value: 0 })));
  };
  
  const totalCount = counters.reduce((sum, counter) => sum + counter.value, 0);
  
  return (
    <div className="multi-counter-container">
      <h1>多计数器管理</h1>
      
      <div className="total-display">
        <h2>总计:{totalCount}</h2>
      </div>
      
      <div className="global-controls">
        <button onClick={addCounter}>添加计数器</button>
        <button onClick={resetAll}>全部重置</button>
      </div>
      
      <div className="counters-grid">
        {counters.map(counter => (
          <div key={counter.id} className="counter-card">
            <h3>{counter.name}</h3>
            <p className="value">{counter.value}</p>
            
            <div className="counter-buttons">
              <button onClick={() => updateCounter(counter.id, -1)}>-</button>
              <button onClick={() => resetCounter(counter.id)}>重置</button>
              <button onClick={() => updateCounter(counter.id, 1)}>+</button>
            </div>
            
            <button
              className="remove-btn"
              onClick={() => removeCounter(counter.id)}
              disabled={counters.length === 1}
            >
              删除
            </button>
          </div>
        ))}
      </div>
    </div>
  );
}

第三部分:高级特性

3.1 定时器计数器

jsx
function TimerCounter() {
  const [count, setCount] = useState(0);
  const [isRunning, setIsRunning] = useState(false);
  const [interval, setIntervalDuration] = useState(1000);
  const timerRef = useRef(null);
  
  useEffect(() => {
    if (isRunning) {
      timerRef.current = setInterval(() => {
        setCount(c => c + 1);
      }, interval);
    } else {
      if (timerRef.current) {
        clearInterval(timerRef.current);
      }
    }
    
    return () => {
      if (timerRef.current) {
        clearInterval(timerRef.current);
      }
    };
  }, [isRunning, interval]);
  
  const start = () => setIsRunning(true);
  const pause = () => setIsRunning(false);
  const reset = () => {
    setIsRunning(false);
    setCount(0);
  };
  
  return (
    <div className="timer-counter">
      <h1>定时器计数器</h1>
      
      <p className="counter-display">{count}</p>
      
      <div className="interval-control">
        <label>间隔(毫秒):</label>
        <input
          type="number"
          value={interval}
          onChange={(e) => setIntervalDuration(Number(e.target.value))}
          min="100"
          max="5000"
          step="100"
          disabled={isRunning}
        />
      </div>
      
      <div className="timer-buttons">
        {!isRunning ? (
          <button onClick={start} className="btn-success">开始</button>
        ) : (
          <button onClick={pause} className="btn-warning">暂停</button>
        )}
        <button onClick={reset} className="btn-danger">重置</button>
      </div>
    </div>
  );
}

3.2 目标计数器

jsx
function GoalCounter() {
  const [count, setCount] = useState(0);
  const [goal, setGoal] = useState(100);
  const [startTime, setStartTime] = useState(null);
  const [endTime, setEndTime] = useState(null);
  
  useEffect(() => {
    if (count === 1 && startTime === null) {
      setStartTime(Date.now());
    }
    
    if (count === goal && endTime === null) {
      setEndTime(Date.now());
    }
  }, [count, goal, startTime, endTime]);
  
  const increment = () => {
    if (count < goal) {
      setCount(count + 1);
    }
  };
  
  const reset = () => {
    setCount(0);
    setStartTime(null);
    setEndTime(null);
  };
  
  const progress = (count / goal) * 100;
  const timeElapsed = endTime
    ? ((endTime - startTime) / 1000).toFixed(2)
    : startTime
    ? ((Date.now() - startTime) / 1000).toFixed(2)
    : 0;
  
  const isCompleted = count === goal;
  
  return (
    <div className="goal-counter">
      <h1>目标计数器</h1>
      
      <div className="goal-input">
        <label>目标:</label>
        <input
          type="number"
          value={goal}
          onChange={(e) => setGoal(Number(e.target.value))}
          min="1"
          disabled={count > 0}
        />
      </div>
      
      <div className="progress-section">
        <p className="progress-text">
          进度:{count} / {goal} ({progress.toFixed(1)}%)
        </p>
        
        <div className="progress-bar">
          <div
            className="progress-fill"
            style={{
              width: `${progress}%`,
              background: isCompleted ? '#28a745' : '#007bff'
            }}
          />
        </div>
      </div>
      
      {isCompleted && (
        <div className="completion-message">
          🎉 完成!用时:{timeElapsed} 秒
        </div>
      )}
      
      {!isCompleted && startTime && (
        <div className="elapsed-time">
          已用时:{timeElapsed} 秒
        </div>
      )}
      
      <div className="counter-buttons">
        <button
          onClick={increment}
          disabled={isCompleted}
          className="btn-primary"
        >
          +1
        </button>
        <button onClick={reset} className="btn-secondary">
          重置
        </button>
      </div>
    </div>
  );
}

3.3 竞赛计数器

jsx
function RaceCounter() {
  const [players, setPlayers] = useState([
    { id: 1, name: '玩家1', count: 0 },
    { id: 2, name: '玩家2', count: 0 }
  ]);
  const [goal, setGoal] = useState(50);
  const [winner, setWinner] = useState(null);
  const [gameStarted, setGameStarted] = useState(false);
  
  const incrementPlayer = (id) => {
    if (winner) return;
    
    setPlayers(prev => prev.map(player => {
      if (player.id === id) {
        const newCount = player.count + 1;
        if (newCount >= goal && !winner) {
          setWinner(player.name);
        }
        return { ...player, count: newCount };
      }
      return player;
    }));
  };
  
  const resetGame = () => {
    setPlayers(players.map(p => ({ ...p, count: 0 })));
    setWinner(null);
    setGameStarted(false);
  };
  
  const startGame = () => {
    setGameStarted(true);
  };
  
  return (
    <div className="race-counter">
      <h1>竞赛计数器</h1>
      
      {!gameStarted && (
        <div className="setup">
          <div className="goal-input">
            <label>目标分数:</label>
            <input
              type="number"
              value={goal}
              onChange={(e) => setGoal(Number(e.target.value))}
              min="10"
              max="100"
            />
          </div>
          <button onClick={startGame} className="btn-success">
            开始游戏
          </button>
        </div>
      )}
      
      {gameStarted && (
        <>
          {winner && (
            <div className="winner-announcement">
              🏆 {winner} 获胜!
            </div>
          )}
          
          <div className="players-container">
            {players.map(player => {
              const progress = (player.count / goal) * 100;
              return (
                <div key={player.id} className="player-lane">
                  <h3>{player.name}</h3>
                  
                  <div className="player-progress">
                    <div className="progress-bar">
                      <div
                        className="progress-fill"
                        style={{ width: `${progress}%` }}
                      />
                    </div>
                    <span className="progress-label">
                      {player.count} / {goal}
                    </span>
                  </div>
                  
                  <button
                    onClick={() => incrementPlayer(player.id)}
                    disabled={!!winner}
                    className="btn-primary"
                  >
                    点击 +1
                  </button>
                </div>
              );
            })}
          </div>
          
          <button onClick={resetGame} className="btn-secondary">
            重新开始
          </button>
        </>
      )}
    </div>
  );
}

第四部分:组件拆分与优化

4.1 拆分CounterDisplay组件

jsx
// CounterDisplay.jsx
function CounterDisplay({ value, className }) {
  return (
    <p className={`counter-display ${className || ''}`}>
      {value}
    </p>
  );
}

export default CounterDisplay;

4.2 拆分CounterButtons组件

jsx
// CounterButtons.jsx
function CounterButtons({ onIncrement, onDecrement, onReset }) {
  return (
    <div className="counter-buttons">
      <button onClick={onDecrement} className="btn btn-danger">
        -1
      </button>
      <button onClick={onReset} className="btn btn-secondary">
        重置
      </button>
      <button onClick={onIncrement} className="btn btn-success">
        +1
      </button>
    </div>
  );
}

export default CounterButtons;

4.3 使用拆分的组件

jsx
import CounterDisplay from './CounterDisplay';
import CounterButtons from './CounterButtons';

function OptimizedCounter() {
  const [count, setCount] = useState(0);
  
  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  const reset = () => setCount(0);
  
  const displayClass = count > 0 ? 'positive' : count < 0 ? 'negative' : '';
  
  return (
    <div className="counter-container">
      <h1>优化的计数器</h1>
      
      <CounterDisplay value={count} className={displayClass} />
      
      <CounterButtons
        onIncrement={increment}
        onDecrement={decrement}
        onReset={reset}
      />
    </div>
  );
}

4.4 使用React.memo优化

jsx
// CounterDisplay.jsx(优化版)
import { memo } from 'react';

const CounterDisplay = memo(function CounterDisplay({ value, className }) {
  console.log('CounterDisplay渲染');
  
  return (
    <p className={`counter-display ${className || ''}`}>
      {value}
    </p>
  );
});

export default CounterDisplay;

// CounterButtons.jsx(优化版)
import { memo } from 'react';

const CounterButtons = memo(function CounterButtons({ 
  onIncrement, 
  onDecrement, 
  onReset 
}) {
  console.log('CounterButtons渲染');
  
  return (
    <div className="counter-buttons">
      <button onClick={onDecrement} className="btn btn-danger">
        -1
      </button>
      <button onClick={onReset} className="btn btn-secondary">
        重置
      </button>
      <button onClick={onIncrement} className="btn btn-success">
        +1
      </button>
    </div>
  );
});

export default CounterButtons;

// 主组件(优化版)
import { useState, useCallback } from 'react';
import CounterDisplay from './CounterDisplay';
import CounterButtons from './CounterButtons';

function OptimizedCounter() {
  const [count, setCount] = useState(0);
  
  // 使用useCallback缓存函数引用
  const increment = useCallback(() => {
    setCount(c => c + 1);
  }, []);
  
  const decrement = useCallback(() => {
    setCount(c => c - 1);
  }, []);
  
  const reset = useCallback(() => {
    setCount(0);
  }, []);
  
  const displayClass = count > 0 ? 'positive' : count < 0 ? 'negative' : '';
  
  return (
    <div className="counter-container">
      <h1>优化的计数器</h1>
      
      <CounterDisplay value={count} className={displayClass} />
      
      <CounterButtons
        onIncrement={increment}
        onDecrement={decrement}
        onReset={reset}
      />
    </div>
  );
}

第五部分:本地存储

5.1 保存到localStorage

jsx
function PersistentCounter() {
  const [count, setCount] = useState(() => {
    // 从localStorage初始化
    const saved = localStorage.getItem('counter');
    return saved ? JSON.parse(saved) : 0;
  });
  
  // 每次count变化时保存
  useEffect(() => {
    localStorage.setItem('counter', JSON.stringify(count));
  }, [count]);
  
  const increment = () => setCount(c => c + 1);
  const decrement = () => setCount(c => c - 1);
  const reset = () => setCount(0);
  
  return (
    <div className="counter-container">
      <h1>持久化计数器</h1>
      <p className="info">计数会自动保存到本地</p>
      
      <CounterDisplay value={count} />
      <CounterButtons
        onIncrement={increment}
        onDecrement={decrement}
        onReset={reset}
      />
    </div>
  );
}

5.2 自定义Hook封装localStorage

jsx
// useLocalStorage.js
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 {
      const valueToStore = value instanceof Function ? value(storedValue) : value;
      setStoredValue(valueToStore);
      window.localStorage.setItem(key, JSON.stringify(valueToStore));
    } catch (error) {
      console.error(error);
    }
  };
  
  return [storedValue, setValue];
}

// 使用
function CounterWithCustomHook() {
  const [count, setCount] = useLocalStorage('counter', 0);
  
  return (
    <div className="counter-container">
      <h1>计数器</h1>
      <p>{count}</p>
      <button onClick={() => setCount(c => c + 1)}>+1</button>
      <button onClick={() => setCount(c => c - 1)}>-1</button>
      <button onClick={() => setCount(0)}>重置</button>
    </div>
  );
}

第六部分:动画效果

6.1 数字翻转动画

jsx
function AnimatedCounter() {
  const [count, setCount] = useState(0);
  const [isAnimating, setIsAnimating] = useState(false);
  
  const increment = () => {
    setIsAnimating(true);
    setCount(c => c + 1);
    setTimeout(() => setIsAnimating(false), 300);
  };
  
  const decrement = () => {
    setIsAnimating(true);
    setCount(c => c - 1);
    setTimeout(() => setIsAnimating(false), 300);
  };
  
  return (
    <div className="counter-container">
      <h1>动画计数器</h1>
      
      <p
        className={`counter-display ${isAnimating ? 'animate' : ''}`}
        style={{
          transition: 'all 0.3s ease',
          transform: isAnimating ? 'scale(1.2)' : 'scale(1)'
        }}
      >
        {count}
      </p>
      
      <div className="counter-buttons">
        <button onClick={decrement}>-1</button>
        <button onClick={() => setCount(0)}>重置</button>
        <button onClick={increment}>+1</button>
      </div>
    </div>
  );
}

6.2 进度条动画

jsx
function ProgressCounter() {
  const [count, setCount] = useState(0);
  const max = 100;
  const progress = (count / max) * 100;
  
  return (
    <div className="counter-container">
      <h1>进度计数器</h1>
      
      <p className="counter-display">{count} / {max}</p>
      
      <div className="progress-container">
        <div
          className="progress-bar"
          style={{
            width: `${progress}%`,
            transition: 'width 0.3s ease',
            background: `hsl(${progress * 1.2}, 70%, 50%)`
          }}
        />
      </div>
      
      <div className="counter-buttons">
        <button
          onClick={() => setCount(Math.max(0, count - 1))}
          disabled={count === 0}
        >
          -1
        </button>
        <button onClick={() => setCount(0)}>重置</button>
        <button
          onClick={() => setCount(Math.min(max, count + 1))}
          disabled={count === max}
        >
          +1
        </button>
      </div>
    </div>
  );
}

第七部分:测试

7.1 单元测试

jsx
// Counter.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

describe('Counter组件', () => {
  test('初始计数为0', () => {
    render(<Counter />);
    const display = screen.getByText('0');
    expect(display).toBeInTheDocument();
  });
  
  test('点击+1按钮增加计数', () => {
    render(<Counter />);
    const incrementBtn = screen.getByText('+1');
    
    fireEvent.click(incrementBtn);
    expect(screen.getByText('1')).toBeInTheDocument();
    
    fireEvent.click(incrementBtn);
    expect(screen.getByText('2')).toBeInTheDocument();
  });
  
  test('点击-1按钮减少计数', () => {
    render(<Counter />);
    const decrementBtn = screen.getByText('-1');
    
    fireEvent.click(decrementBtn);
    expect(screen.getByText('-1')).toBeInTheDocument();
  });
  
  test('点击重置按钮归零', () => {
    render(<Counter />);
    const incrementBtn = screen.getByText('+1');
    const resetBtn = screen.getByText('重置');
    
    fireEvent.click(incrementBtn);
    fireEvent.click(incrementBtn);
    expect(screen.getByText('2')).toBeInTheDocument();
    
    fireEvent.click(resetBtn);
    expect(screen.getByText('0')).toBeInTheDocument();
  });
});

7.2 集成测试

jsx
// Counter.integration.test.jsx
import { render, screen, fireEvent } from '@testing-library/react';
import Counter from './Counter';

describe('Counter集成测试', () => {
  test('完整操作流程', () => {
    render(<Counter />);
    
    // 初始为0
    expect(screen.getByText('0')).toBeInTheDocument();
    
    // 增加到5
    const incrementBtn = screen.getByText('+1');
    for (let i = 0; i < 5; i++) {
      fireEvent.click(incrementBtn);
    }
    expect(screen.getByText('5')).toBeInTheDocument();
    
    // 减少2
    const decrementBtn = screen.getByText('-1');
    fireEvent.click(decrementBtn);
    fireEvent.click(decrementBtn);
    expect(screen.getByText('3')).toBeInTheDocument();
    
    // 重置
    const resetBtn = screen.getByText('重置');
    fireEvent.click(resetBtn);
    expect(screen.getByText('0')).toBeInTheDocument();
  });
});

第八部分:React 19特性

8.1 使用Server Actions

jsx
// app/actions/counter.js
'use server';

export async function incrementCounter(currentCount) {
  // 可以在这里添加服务端逻辑
  await new Promise(resolve => setTimeout(resolve, 100));
  return currentCount + 1;
}

export async function saveCounter(count) {
  'use server';
  // 保存到数据库
  await db.counters.update({ value: count });
  return { success: true };
}

// app/components/ServerCounter.jsx
'use client';

import { useState, useTransition } from 'react';
import { incrementCounter, saveCounter } from '../actions/counter';

function ServerCounter() {
  const [count, setCount] = useState(0);
  const [isPending, startTransition] = useTransition();
  
  const handleIncrement = async () => {
    startTransition(async () => {
      const newCount = await incrementCounter(count);
      setCount(newCount);
    });
  };
  
  const handleSave = async () => {
    startTransition(async () => {
      await saveCounter(count);
      alert('保存成功!');
    });
  };
  
  return (
    <div className="counter-container">
      <h1>Server Actions计数器</h1>
      
      <p className="counter-display">{count}</p>
      
      <div className="counter-buttons">
        <button onClick={handleIncrement} disabled={isPending}>
          {isPending ? '处理中...' : '+1'}
        </button>
        <button onClick={handleSave} disabled={isPending}>
          保存
        </button>
      </div>
    </div>
  );
}

8.2 使用useOptimistic

jsx
'use client';

import { useState, useOptimistic } from 'react';

function OptimisticCounter() {
  const [count, setCount] = useState(0);
  const [optimisticCount, setOptimisticCount] = useOptimistic(count);
  
  const increment = async () => {
    // 立即更新UI(乐观更新)
    setOptimisticCount(count + 1);
    
    // 异步调用服务器
    try {
      const response = await fetch('/api/counter/increment', {
        method: 'POST',
        body: JSON.stringify({ value: count })
      });
      const data = await response.json();
      setCount(data.value);
    } catch (error) {
      // 如果失败,恢复原值
      setOptimisticCount(count);
    }
  };
  
  return (
    <div className="counter-container">
      <h1>乐观更新计数器</h1>
      
      <p className="counter-display">{optimisticCount}</p>
      
      <button onClick={increment}>+1</button>
    </div>
  );
}

第九部分:最佳实践总结

9.1 State管理

jsx
// ✅ 好的做法:使用函数式更新
const increment = () => {
  setCount(c => c + 1);
};

// ❌ 避免:直接使用count
const increment = () => {
  setCount(count + 1);
};

9.2 事件处理

jsx
// ✅ 好的做法:提取处理函数
function Counter() {
  const handleIncrement = () => setCount(c => c + 1);
  return <button onClick={handleIncrement}>+1</button>;
}

// ❌ 避免:内联箭头函数(在大列表中)
function Counter() {
  return <button onClick={() => setCount(c => c + 1)}>+1</button>;
}

9.3 性能优化

jsx
// ✅ 使用useCallback缓存函数
const increment = useCallback(() => {
  setCount(c => c + 1);
}, []);

// ✅ 使用React.memo缓存组件
const CounterDisplay = memo(({ value }) => (
  <p>{value}</p>
));

练习题

基础练习

  1. 创建一个基础计数器,包含增加、减少、重置功能
  2. 添加样式,让计数器看起来美观
  3. 实现步长控制功能

进阶练习

  1. 实现范围限制计数器
  2. 添加历史记录和撤销/重做功能
  3. 创建多个独立的计数器

高级练习

  1. 实现定时器自动计数
  2. 创建目标计数器,显示进度和用时
  3. 实现竞赛计数器,支持多玩家对战
  4. 添加本地存储,刷新页面后数据不丢失
  5. 编写完整的单元测试和集成测试

挑战练习

  1. 使用React 19的Server Actions实现服务端计数
  2. 实现乐观更新,提升用户体验
  3. 添加复杂的动画效果
  4. 创建一个可配置的计数器组件库

总结

通过本章的学习,你已经:

  • 掌握了useState的基本用法和高级技巧
  • 学会了事件处理的各种模式
  • 理解了组件拆分和复用
  • 掌握了性能优化方法
  • 学会了使用localStorage持久化数据
  • 了解了组件测试的方法
  • 体验了React 19的新特性

计数器虽然简单,但包含了React开发的核心概念。掌握这些知识,你就可以开始构建更复杂的应用了!

继续学习,探索更多React的强大功能!