React/NextJS useEffect更新数组指定元素百分比计算问题
投手计数应用命中率计算失效修复
核心问题定位
- 计算时机错误:原有逻辑在更新命中/未命中数值之前就计算百分比,用的是旧的计数值,永远得到上一次的命中率结果
- 状态更新存在闭包问题:
generateId函数调用setId后直接返回旧id值,会导致多选手id重复 useEffect依赖配置错误:原有effect依赖handleHit/handleMiss函数,这两个函数每次组件渲染都会重新生成,无法在计数变化时触发计算- 冗余字段问题:
handleHit里写入了接口未定义的setCountHit字段,属于无效代码
修复步骤
按照要求将命中率计算逻辑迁移到useEffect中实现:
- 简化
handleHit、handleMiss逻辑,仅负责更新对应选手的命中/未命中计数,不再同步计算百分比 - 修复id生成的闭包问题,使用函数式更新保证自增id唯一
- 新增专门的
useEffect监听计数变化,遍历所有选手数据,基于最新的命中、未命中值重新计算命中率后更新状态 - 修正
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 <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
相关产品推荐
相关产品推荐

