Skip to content

第15章:进阶实战:计数器(Hooks 综合应用)

计数器是一个简单但非常适合学习 React Hooks 的实战项目。通过实现一个功能完整的计数器,我们可以巩固对 useStateuseEffect 等 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 应用打下了基础。

© 2026 编程马·菜鸟教程 版权所有