You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

React/NextJS useEffect更新数组指定元素百分比计算问题

投手计数应用命中率计算失效修复

核心问题定位

  • 计算时机错误:原有逻辑在更新命中/未命中数值之前就计算百分比,用的是旧的计数值,永远得到上一次的命中率结果
  • 状态更新存在闭包问题:generateId 函数调用setId后直接返回旧id值,会导致多选手id重复
  • useEffect依赖配置错误:原有effect依赖handleHit/handleMiss函数,这两个函数每次组件渲染都会重新生成,无法在计数变化时触发计算
  • 冗余字段问题:handleHit里写入了接口未定义的setCountHit字段,属于无效代码

修复步骤

按照要求将命中率计算逻辑迁移到useEffect中实现:

  1. 简化handleHit、handleMiss逻辑,仅负责更新对应选手的命中/未命中计数,不再同步计算百分比
  2. 修复id生成的闭包问题,使用函数式更新保证自增id唯一
  3. 新增专门的useEffect监听计数变化,遍历所有选手数据,基于最新的命中、未命中值重新计算命中率后更新状态
  4. 修正useEffect依赖项,仅在计数数据实际变化时触发重算,避免无限循环

修复后完整代码

import React, { useEffect, useState } from 'react';
import Counter from './components/counter';
import { NextPage } from 'next';

interface CountersProps {
  id: number;
  countHit: number;
  countMiss: number;
  percentage: string;
}

const Match: NextPage = () => {
  const [countersData, setCountersData] = useState<Array<CountersProps>>([]);
  const [id, setId] = useState(1);

  // 修复id生成闭包问题
  const generateId = () => {
    const newId = id + 1;
    setId(newId);
    return newId;
  };

  const handleHit = (targetId: number) => {
    setCountersData(prev => prev.map(counter => {
      if (counter.id !== targetId) return counter;
      // 仅更新命中数,不在事件处理函数中计算百分比
      return {
        ...counter,
        countHit: counter.countHit + 1
      }
    }))
  };

  const handleMiss = (targetId: number) => {
    setCountersData(prev => prev.map(counter => {
      if (counter.id !== targetId) return counter;
      // 仅更新未命中数,不在事件处理函数中计算百分比
      return {
        ...counter,
        countMiss: counter.countMiss + 1
      }
    }))
  };

  const addShooter = () => {
    setCountersData(prev => [
      ...prev,
      { id: generateId(), countHit: 0, countMiss: 0, percentage: '0%' },
    ]);
  };

  // 命中率计算逻辑统一放在useEffect中
  useEffect(() => {
    setCountersData(prev => prev.map(counter => {
      const total = counter.countHit + counter.countMiss;
      // 总出手数为0时直接返回0%,避免除以0出现NaN异常
      const newPercentage = total === 0 ? '0%' : `${((counter.countHit / total) * 100).toFixed(1)}%`;
      return {
        ...counter,
        percentage: newPercentage
      }
    }))
  // 依赖项为所有选手的计数值拼接结果,仅当计数实际变化时触发重算
  }, [countersData.map(c => `${c.id}-${c.countHit}-${c.countMiss}`).join(',')])

  return (
    <main className="flex flex-col items-center justify-center flex-1 w-full px-20 text-center">
      <h1 className="text-6xl font-extrabold">
        The&nbsp;
        <span
          className="text-transparent bg-clip-text bg-gradient-to-r from-indigo-700 to-emerald-700"
        >
          Netball Counter
        </span>
      </h1>
      <div className="mt-10">
        <button
          type="button"
          className="inline-flex items-center px-4 py-2 text-base font-medium text-white bg-indigo-600 border border-transparent rounded-md shadow-sm hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
          onClick={() => addShooter()}
        >
          Add Shooter to Match
        </button>
      </div>
      <div className="mt-10">
        {countersData.map((counter) => (
          <Counter
            key={counter.id}
            {...counter}
            name="Mia"
            handleHit={handleHit}
            handleMiss={handleMiss}
          />
        ))}
      </div>
    </main>
  );
};

export default Match;

额外优化点

  • 新增总出手数为0的边界判断,避免首次加载时出现NaN%的异常显示
  • 给百分比增加1位小数的格式化,赛事场景下展示更精准
  • 所有状态更新改用函数式写法,彻底规避闭包导致的旧值引用问题
  • 移除了无效的冗余字段和不必要的参数传递

内容的提问来源于stack exchange,提问作者tuppers

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.30 13:18:23