Appearance
第15章:进阶实战:计数器(Hooks 综合应用)
计数器是一个简单但非常适合学习 React Hooks 的实战项目。通过实现一个功能完整的计数器,我们可以巩固对 useState、useEffect 等 Hooks 的理解,并且学习如何创建和使用自定义 Hooks。本章将详细介绍如何构建一个功能丰富的计数器应用。
15.1 需求分析(计数、增减、重置、步进设置)
15.1.1 功能需求
- 显示当前计数
- 增加计数
- 减少计数
- 重置计数到初始值
- 设置步进值(每次增减的数量)
- 响应式设计,适配不同屏幕尺寸
- 动画效果,提升用户体验
15.1.2 页面结构设计
Counter
├── 计数显示区域
├── 控制按钮区域
│ ├── 减少按钮
│ ├── 重置按钮
│ └── 增加按钮
└── 步进设置区域
├── 标签
└── 输入框15.2 useState + useEffect 综合使用
首先,我们来创建一个基础的计数器组件,使用 useState 管理状态,使用 useEffect 处理副作用。
jsx
// src/components/Counter.js
import React, { useState, useEffect } from 'react';
import './Counter.css';
function Counter() {
const [count, setCount] = useState(0);
const [step, setStep] = useState(1);
const [initialValue, setInitialValue] = useState(0);
// 当步进值变化时,记录到控制台
useEffect(() => {
console.log(`步进值已更新为: ${step}`);
}, [step]);
// 当计数变化时,更新文档标题
useEffect(() => {
document.title = `计数器: ${count}`;
}, [count]);
const handleIncrement = () => {
setCount(prevCount => prevCount + step);
};
const handleDecrement = () => {
setCount(prevCount => prevCount - step);
};
const handleReset = () => {
setCount(initialValue);
};
const handleStepChange = (e) => {
const newStep = parseInt(e.target.value) || 1;
setStep(newStep);
};
return (
<div className="counter-container">
<h1>计数器</h1>
<div className="count-display">
<span className="count-value">{count}</span>
</div>
<div className="controls">
<button
className="control-button decrement"
onClick={handleDecrement}
>
-
</button>
<button
className="control-button reset"
onClick={handleReset}
>
重置
</button>
<button
className="control-button increment"
onClick={handleIncrement}
>
+
</button>
</div>
<div className="step-control">
<label htmlFor="step">步进值:</label>
<input
type="number"
id="step"
min="1"
value={step}
onChange={handleStepChange}
className="step-input"
/>
</div>
</div>
);
}
export default Counter;15.2.1 CSS 样式
css
/* src/components/Counter.css */
.counter-container {
max-width: 400px;
margin: 50px auto;
padding: 30px;
background-color: white;
border-radius: 10px;
box-shadow: 0 4px 15px rgba(0, 0, 0, 0.1);
text-align: center;
}
.counter-container h1 {
color: #333;
margin-bottom: 30px;
font-size: 28px;
}
.count-display {
margin: 40px 0;
padding: 30px;
background-color: #f5f5f5;
border-radius: 8px;
box-shadow: inset 0 2px 5px rgba(0, 0, 0, 0.1);
}
.count-value {
font-size: 64px;
font-weight: bold;
color: #4CAF50;
transition: all 0.3s ease;
}
.count-value:hover {
transform: scale(1.05);
}
.controls {
display: flex;
justify-content: space-between;
margin: 30px 0;
}
.control-button {
flex: 1;
padding: 15px;
font-size: 24px;
font-weight: bold;
border: none;
border-radius: 6px;
cursor: pointer;
transition: all 0.3s ease;
margin: 0 5px;
}
.control-button:hover {
transform: translateY(-2px);
box-shadow: 0 4px 8px rgba(0, 0, 0, 0.1);
}
.decrement {
background-color: #f44336;
color: white;
}
.decrement:hover {
background-color: #d32f2f;
}
.reset {
background-color: #ff9800;
color: white;
}
.reset:hover {
background-color: #f57c00;
}
.increment {
background-color: #4CAF50;
color: white;
}
.increment:hover {
background-color: #45a049;
}
.step-control {
margin-top: 30px;
display: flex;
align-items: center;
justify-content: center;
}
.step-control label {
margin-right: 10px;
font-size: 16px;
color: #666;
}
.step-input {
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 16px;
width: 80px;
text-align: center;
}
.step-input:focus {
outline: none;
border-color: #4CAF50;
box-shadow: 0 0 0 2px rgba(76, 175, 80, 0.2);
}
/* 响应式设计 */
@media (max-width: 480px) {
.counter-container {
margin: 20px;
padding: 20px;
}
.count-value {
font-size: 48px;
}
.control-button {
padding: 12px;
font-size: 20px;
}
}15.3 自定义Hooks封装(useCounter)
现在,我们将计数器的逻辑提取到一个自定义 Hook 中,以便在其他组件中复用。
15.3.1 创建 useCounter 自定义 Hook
jsx
// src/hooks/useCounter.js
import { useState, useEffect, useCallback } from 'react';
export function useCounter(initialValue = 0, defaultStep = 1) {
const [count, setCount] = useState(initialValue);
const [step, setStep] = useState(defaultStep);
// 增加计数
const increment = useCallback(() => {
setCount(prevCount => prevCount + step);
}, [step]);
// 减少计数
const decrement = useCallback(() => {
setCount(prevCount => prevCount - step);
}, [step]);
// 重置计数
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
// 设置步进值
const setCounterStep = useCallback((newStep) => {
setStep(newStep);
}, []);
// 直接设置计数
const setCounterValue = useCallback((newValue) => {
setCount(newValue);
}, []);
return {
count,
step,
increment,
decrement,
reset,
setStep: setCounterStep,
setCount: setCounterValue
};
}15.3.2 使用自定义 Hook
现在,我们可以使用自定义的 useCounter Hook 来重构我们的计数器组件:
jsx
// src/components/CounterWithHook.js
import React from 'react';
import { useCounter } from '../hooks/useCounter';
import './Counter.css';
function CounterWithHook() {
const {
count,
step,
increment,
decrement,
reset,
setStep
} = useCounter(0, 1);
const handleStepChange = (e) => {
const newStep = parseInt(e.target.value) || 1;
setStep(newStep);
};
return (
<div className="counter-container">
<h1>计数器 (使用自定义 Hook)</h1>
<div className="count-display">
<span className="count-value">{count}</span>
</div>
<div className="controls">
<button
className="control-button decrement"
onClick={decrement}
>
-
</button>
<button
className="control-button reset"
onClick={reset}
>
重置
</button>
<button
className="control-button increment"
onClick={increment}
>
+
</button>
</div>
<div className="step-control">
<label htmlFor="step">步进值:</label>
<input
type="number"
id="step"
min="1"
value={step}
onChange={handleStepChange}
className="step-input"
/>
</div>
</div>
);
}
export default CounterWithHook;15.4 表单绑定(步进值设置)
在上面的示例中,我们已经实现了步进值的表单绑定。现在,我们来扩展这个功能,添加更多的表单控制选项,例如初始值设置和计数范围限制。
15.4.1 扩展计数器组件
jsx
// src/components/AdvancedCounter.js
import React, { useState } from 'react';
import { useCounter } from '../hooks/useCounter';
import './Counter.css';
function AdvancedCounter() {
const [initialValue, setInitialValue] = useState(0);
const [minValue, setMinValue] = useState(-100);
const [maxValue, setMaxValue] = useState(100);
// 使用扩展的 useCounter Hook
const {
count,
step,
increment,
decrement,
reset,
setStep,
setCount
} = useCounter(initialValue, 1);
const handleStepChange = (e) => {
const newStep = parseInt(e.target.value) || 1;
setStep(newStep);
};
const handleInitialValueChange = (e) => {
const newValue = parseInt(e.target.value) || 0;
setInitialValue(newValue);
};
const handleMinValueChange = (e) => {
const newValue = parseInt(e.target.value) || -100;
setMinValue(newValue);
};
const handleMaxValueChange = (e) => {
const newValue = parseInt(e.target.value) || 100;
setMaxValue(newValue);
};
const handleSetInitialValue = () => {
setCount(initialValue);
};
// 带范围限制的增减函数
const handleIncrementWithLimit = () => {
if (count < maxValue) {
increment();
}
};
const handleDecrementWithLimit = () => {
if (count > minValue) {
decrement();
}
};
return (
<div className="counter-container">
<h1>高级计数器</h1>
<div className="count-display">
<span className={`count-value ${count >= maxValue ? 'max-reached' : count <= minValue ? 'min-reached' : ''}`}>
{count}
</span>
<p className="count-range">
范围: {minValue} 到 {maxValue}
</p>
</div>
<div className="controls">
<button
className="control-button decrement"
onClick={handleDecrementWithLimit}
disabled={count <= minValue}
>
-
</button>
<button
className="control-button reset"
onClick={reset}
>
重置
</button>
<button
className="control-button increment"
onClick={handleIncrementWithLimit}
disabled={count >= maxValue}
>
+
</button>
</div>
<div className="form-controls">
<div className="form-group">
<label htmlFor="step">步进值:</label>
<input
type="number"
id="step"
min="1"
value={step}
onChange={handleStepChange}
className="step-input"
/>
</div>
<div className="form-group">
<label htmlFor="initial-value">初始值:</label>
<input
type="number"
id="initial-value"
value={initialValue}
onChange={handleInitialValueChange}
className="step-input"
/>
<button
className="set-button"
onClick={handleSetInitialValue}
>
设置
</button>
</div>
<div className="form-group">
<label htmlFor="min-value">最小值:</label>
<input
type="number"
id="min-value"
value={minValue}
onChange={handleMinValueChange}
className="step-input"
/>
</div>
<div className="form-group">
<label htmlFor="max-value">最大值:</label>
<input
type="number"
id="max-value"
value={maxValue}
onChange={handleMaxValueChange}
className="step-input"
/>
</div>
</div>
</div>
);
}
export default AdvancedCounter;15.4.2 扩展 CSS 样式
css
/* src/components/Counter.css 中添加以下样式 */
.count-range {
margin-top: 10px;
font-size: 14px;
color: #666;
}
.count-value.max-reached {
color: #f44336;
animation: pulse 0.5s ease-in-out;
}
.count-value.min-reached {
color: #2196F3;
animation: pulse 0.5s ease-in-out;
}
@keyframes pulse {
0% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
100% {
transform: scale(1);
}
}
.form-controls {
margin-top: 30px;
display: flex;
flex-direction: column;
gap: 15px;
}
.form-group {
display: flex;
align-items: center;
justify-content: space-between;
}
.form-group label {
font-size: 14px;
color: #666;
width: 80px;
text-align: left;
}
.set-button {
background-color: #2196F3;
color: white;
border: none;
border-radius: 4px;
padding: 6px 12px;
font-size: 14px;
cursor: pointer;
margin-left: 10px;
}
.set-button:hover {
background-color: #0b7dda;
}
button:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
box-shadow: none !important;
}
button:disabled:hover {
background-color: inherit;
}
/* 响应式设计调整 */
@media (max-width: 480px) {
.form-group {
flex-direction: column;
align-items: flex-start;
gap: 5px;
}
.form-group label {
width: 100%;
}
.form-group input {
width: 100%;
}
.set-button {
margin-left: 0;
margin-top: 5px;
}
}15.5 自定义 Hook 的进阶应用
我们可以进一步扩展 useCounter Hook,添加更多功能,例如:
15.5.1 带有本地存储的 useCounter Hook
jsx
// src/hooks/useCounterWithStorage.js
import { useState, useEffect, useCallback } from 'react';
export function useCounterWithStorage(key, initialValue = 0, defaultStep = 1) {
// 从本地存储加载初始值
const [count, setCount] = useState(() => {
const savedCount = localStorage.getItem(key);
return savedCount ? parseInt(savedCount) : initialValue;
});
const [step, setStep] = useState(defaultStep);
// 保存计数到本地存储
useEffect(() => {
localStorage.setItem(key, count.toString());
}, [key, count]);
// 增加计数
const increment = useCallback(() => {
setCount(prevCount => prevCount + step);
}, [step]);
// 减少计数
const decrement = useCallback(() => {
setCount(prevCount => prevCount - step);
}, [step]);
// 重置计数
const reset = useCallback(() => {
setCount(initialValue);
}, [initialValue]);
// 设置步进值
const setCounterStep = useCallback((newStep) => {
setStep(newStep);
}, []);
// 直接设置计数
const setCounterValue = useCallback((newValue) => {
setCount(newValue);
}, []);
return {
count,
step,
increment,
decrement,
reset,
setStep: setCounterStep,
setCount: setCounterValue
};
}15.5.2 使用带有本地存储的 Hook
jsx
// src/components/CounterWithStorage.js
import React from 'react';
import { useCounterWithStorage } from '../hooks/useCounterWithStorage';
import './Counter.css';
function CounterWithStorage() {
const {
count,
step,
increment,
decrement,
reset,
setStep
} = useCounterWithStorage('counter-value', 0, 1);
const handleStepChange = (e) => {
const newStep = parseInt(e.target.value) || 1;
setStep(newStep);
};
return (
<div className="counter-container">
<h1>计数器 (带本地存储)</h1>
<p className="storage-info">计数会保存在本地存储中</p>
<div className="count-display">
<span className="count-value">{count}</span>
</div>
<div className="controls">
<button
className="control-button decrement"
onClick={decrement}
>
-
</button>
<button
className="control-button reset"
onClick={reset}
>
重置
</button>
<button
className="control-button increment"
onClick={increment}
>
+
</button>
</div>
<div className="step-control">
<label htmlFor="step">步进值:</label>
<input
type="number"
id="step"
min="1"
value={step}
onChange={handleStepChange}
className="step-input"
/>
</div>
</div>
);
}
export default CounterWithStorage;15.5.3 添加存储信息样式
css
/* src/components/Counter.css 中添加以下样式 */
.storage-info {
color: #666;
font-size: 14px;
margin-bottom: 20px;
padding: 10px;
background-color: #e3f2fd;
border-radius: 4px;
}小结
本章通过实现一个功能丰富的计数器应用,我们学习了以下内容:
- Hooks 综合应用:使用
useState管理状态,使用useEffect处理副作用 - 自定义 Hooks:创建和使用
useCounter自定义 Hook,封装计数器逻辑 - 表单绑定:实现步进值、初始值等表单控件的绑定
- 功能扩展:添加计数范围限制、本地存储等功能
- 用户体验优化:添加动画效果、禁用状态等
自定义 Hooks 是 React 16.8+ 的重要特性,它允许我们将组件逻辑提取到可重用的函数中,提高代码的可维护性和复用性。通过本章的实战练习,你应该对自定义 Hooks 的创建和使用有了更深入的理解,为后续开发更复杂的 React 应用打下了基础。
