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

React二十一点模拟:数组评估与useState弹窗触发的渲染问题

二十一点手牌模拟程序Ace处理与渲染问题解决

问题根源

  • 总分total是普通变量,每次组件渲染都会重新初始化,无法持久化状态,Popup里修改total也不会同步到父组件
  • 在渲染流程中直接处理Ace判断(比如alert),会导致每次组件渲染都执行该逻辑,一旦触发弹窗/状态更新,又会引发新一轮渲染,最终导致"渲染次数过多"
  • 未跟踪已处理的Ace,导致重复触发选择弹窗

解决方案

  1. 将total改为React状态变量,确保总分能响应式更新并持久化
  2. 添加状态跟踪未处理的Ace(记录当前需要处理的Ace索引),避免重复触发弹窗
  3. 使用useEffect监听手牌变化,当出现未处理的Ace时触发弹窗
  4. Popup组件通过回调函数将用户选择的点数传递回父组件,更新总分并标记该Ace已处理

修改后的代码

Home组件

import { useState, useEffect } from 'react'
import Card from '../components/Card';
import Total from '../components/Total';
import Popup from '../components/Popup'

import { shuffle } from '../hooks/shuffleCards'
import { deckArray } from '../utils/data'

export default function Home() {
    const startHandSize = 2
    const [starterDeck, setStarterDeck] = useState(shuffle(deckArray))
    const [howManyDealt, setHowManyDealt] = useState(startHandSize)
    const [triggerPopup, setTriggerPopup] = useState(false)
    const [total, setTotal] = useState(0)
    // 记录已处理的Ace索引,避免重复弹窗
    const [processedAces, setProcessedAces] = useState([])
    const [currentAceIndex, setCurrentAceIndex] = useState(null)

    const deal = () => {
        setHowManyDealt(startHandSize)
        setStarterDeck(shuffle(deckArray))
        setTotal(0)
        setProcessedAces([])
        setCurrentAceIndex(null)
        setTriggerPopup(false)
    }

    const hit = () => !bust && setHowManyDealt(prev => prev + 1)

    const usersCards = starterDeck.slice(-howManyDealt)
    const bust = total > 21;

    // 处理手牌变化,检查未处理的Ace
    useEffect(() => {
        // 先计算非Ace的基础总分
        let baseTotal = 0
        const unprocessedAces = []
        usersCards.forEach((card, index) => {
            if (card.card !== "A") {
                baseTotal += card.value
            } else if (!processedAces.includes(index)) {
                unprocessedAces.push(index)
            }
        })

        // 如果有未处理的Ace,触发弹窗
        if (unprocessedAces.length > 0 && !triggerPopup) {
            setCurrentAceIndex(unprocessedAces[0])
            setTriggerPopup(true)
        } else if (unprocessedAces.length === 0) {
            // 没有未处理的Ace,直接设置基础总分
            setTotal(baseTotal)
        }
    }, [usersCards, processedAces, triggerPopup])

    // 处理用户选择的Ace点数
    const handleAceChoice = (points) => {
        setTotal(prev => prev + points)
        setProcessedAces(prev => [...prev, currentAceIndex])
        setTriggerPopup(false)
        setCurrentAceIndex(null)
    }

    return (
        <div>
            <button onClick={deal}>DEAL</button>
            <button disabled={bust} onClick={hit}>HIT</button>
            <button disabled={bust}>STAND</button>
            <Total total={total} usersCards={usersCards} />
            <Card usersCards={usersCards} />

            {triggerPopup && <Popup onAceChoice={handleAceChoice} />}
        </div>
    )
}

Popup组件

export default function Popup({ onAceChoice }) {
    return (
        <div className="popup">
            <div className="popup-inner">
                <h4>You've got an Ace. Would you like to collect 1 or 10 points?</h4>
                <button className="btn-popup" onClick={() => onAceChoice(1)}>1 POINT</button>
                <button className="btn-popup" onClick={() => onAceChoice(10)}>10 POINTS</button>
            </div>
        </div>
    )
}

关键改动说明

  • 新增total状态变量,替代原普通变量,确保总分能持久化并触发组件更新
  • 添加processedAces和currentAceIndex,跟踪已处理的Ace,避免重复触发弹窗
  • 将Ace处理逻辑移至useEffect,仅在手牌或已处理Ace列表变化时执行,避免渲染阶段触发状态更新导致无限循环
  • Popup不再直接修改total,而是通过onAceChoice回调将选择的点数传递给父组件,由父组件更新状态
  • 移除了原代码中直接在渲染流程里的alert和弹窗触发逻辑,改为受控的useEffect监听

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.06 23:55:12