React记忆卡牌游戏尝试次数计数器异常问题求助
问题分析与解决方案
一、计数器有时加2的原因
你在setSelection的状态更新回调内部调用了setAttempts,如果项目开启了React严格模式(Create React App等脚手架默认开启),React会为检测副作用重复执行状态更新函数,导致setAttempts被调用两次,计数器因此异常累加。
另外,possibleElements和gameTable定义在组件外部,属于全局变量,当组件重新挂载时,possibleElements已被splice清空,会导致后续生成的游戏表格数据异常,这是潜在的隐性bug。
二、抽离计数函数后按钮无法保持选中的原因
在setSelection的回调中调用increaseAttempts(本质是触发attempts状态更新),会在selection状态更新的同时触发组件重渲染,导致Button组件的isSelected属性依赖的selection还未更新为新值,因此无法保持选中状态。
三、修复方案
1. 用useEffect监听selection变化实现计数
将尝试次数的累加逻辑从setSelection回调中移出,通过useEffect监听selection的长度变化,避免在状态更新函数内部触发其他状态更新:
import { useState, useEffect } from 'react'; import Button from "./Button"; export default function GameBoard() { const [selection, setSelection] = useState([]); const [coupledElements, setCoupledElements] = useState([]); const [isEnabled, setIseEnabled] = useState(true); const [attempts, setAttempts] = useState(0); // 监听selection变化,长度为2时累加尝试次数 useEffect(() => { if (selection.length === 2) { setAttempts(prev => prev + 1); } }, [selection]); function sortCoupledElements(selection) { if (selection[0].value === selection[1].value) { setCoupledElements((prevCoupledElements) => { const newCoupledElements = [...prevCoupledElements, selection[0].index, selection[1].index]; return newCoupledElements; }) } } function whenClicked(index, value) { setSelection((prevSelection) => { const newSelection = [...prevSelection, { index, value }]; if (newSelection.length === 2) { setIseEnabled(false); sortCoupledElements(newSelection); setTimeout(() => { setSelection([]); setIseEnabled(true); }, 1000); } return newSelection; }) } // 其余代码保持不变 }
2. 修复全局变量导致的游戏数据异常
把游戏表格的生成逻辑移到组件内部,用useMemo缓存,确保只在组件挂载时生成一次,避免全局变量带来的数据污染:
import { useState, useEffect, useMemo } from 'react'; import Button from "./Button"; export default function GameBoard() { // ... 状态定义保持不变 // 用useMemo缓存游戏表格,仅组件挂载时生成一次 const gameTable = useMemo(() => { const possibleElements = [1, 2, 3, 4, 5, 6, 7, 8, 1, 2, 3, 4, 5, 6, 7, 8]; function randomNumber() { const randIndex = Math.floor(Math.random() * possibleElements.length); const randomNumber = possibleElements[randIndex]; possibleElements.splice(randIndex, 1); return randomNumber; } return [ [randomNumber(), randomNumber(), randomNumber(), randomNumber()], [randomNumber(), randomNumber(), randomNumber(), randomNumber()], [randomNumber(), randomNumber(), randomNumber(), randomNumber()], [randomNumber(), randomNumber(), randomNumber(), randomNumber()] ]; }, []); // ... 其余代码保持不变 }
3. 额外优化:简化逻辑避免冗余操作
可以把配对、定时器逻辑也移到useEffect中,让代码结构更清晰,同时避免重复操作:
useEffect(() => { if (selection.length === 2) { setAttempts(prev => prev + 1); setIseEnabled(false); // 处理元素配对 if (selection[0].value === selection[1].value) { setCoupledElements(prev => [...prev, selection[0].index, selection[1].index]); } // 1秒后清空选择并恢复按钮可用状态 const timer = setTimeout(() => { setSelection([]); setIseEnabled(true); }, 1000); // 组件卸载时清理定时器 return () => clearTimeout(timer); } }, [selection]); // 简化点击事件函数,增加防重复点击逻辑 function whenClicked(index, value) { if (!isEnabled || selection.some(item => item.index === index) || coupledElements.includes(index)) { return; } setSelection(prev => [...prev, { index, value }]); }
修改后,计数器逻辑完全独立于selection的更新流程,不会再出现异常累加的情况,同时按钮的选中状态也能正常保持。
内容的提问来源于stack exchange,提问作者Aquilaruspante
相关产品推荐
相关产品推荐

