Appearance
计数器应用
学习目标
通过本章实战项目,你将全面掌握:
- 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>
));练习题
基础练习
- 创建一个基础计数器,包含增加、减少、重置功能
- 添加样式,让计数器看起来美观
- 实现步长控制功能
进阶练习
- 实现范围限制计数器
- 添加历史记录和撤销/重做功能
- 创建多个独立的计数器
高级练习
- 实现定时器自动计数
- 创建目标计数器,显示进度和用时
- 实现竞赛计数器,支持多玩家对战
- 添加本地存储,刷新页面后数据不丢失
- 编写完整的单元测试和集成测试
挑战练习
- 使用React 19的Server Actions实现服务端计数
- 实现乐观更新,提升用户体验
- 添加复杂的动画效果
- 创建一个可配置的计数器组件库
总结
通过本章的学习,你已经:
- 掌握了useState的基本用法和高级技巧
- 学会了事件处理的各种模式
- 理解了组件拆分和复用
- 掌握了性能优化方法
- 学会了使用localStorage持久化数据
- 了解了组件测试的方法
- 体验了React 19的新特性
计数器虽然简单,但包含了React开发的核心概念。掌握这些知识,你就可以开始构建更复杂的应用了!
继续学习,探索更多React的强大功能!