简体   繁体   中英

How can I evaluate each element in array and use useState() on one condition?

I'm making a blackjack hand simulation and I've encountered an issue with my code. The game goes like this: users gets two random cards and a total of the points, clicks 'hit' to get another random card from the deck. Now that's all working but there's one more rule: if that card is an Ace, user chooses if they want to get 1 or 10 points. I implemented it before when I only had one card at a time with useEffect, however now I refactored my code and the total isn't kept in useState + the array has two cards that need to evaluated, not the most recent one.

I've tried putting my loop and if statement in a useEffect and conditionally render the Popup to let the user decide (with and without dependencies), but when I put the useState() to trigger the condition, it throws an error that there have been too many renders and I'm not sure why that is.

Here's my Home component:

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, setButtonPopup] = useState(false)

    const deal = () => {
        setHowManyDealt(startHandSize)
        setStarterDeck(shuffle(deckArray))
    }

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

    const usersCards = starterDeck.slice(-howManyDealt)

    var total = 0

    usersCards.forEach(function (arrayItem) {
        if(arrayItem.card === "A"){
            alert("you have an ace") 
        }
        else{
            total += arrayItem.value
        }
    });

    const bust = total > 21;

    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 total={total} setButtonPopup={setButtonPopup}/>}
        </div>
    )
}

and my Popup:

export default function Popup({total, setButtonPopup}){
   const handleClick = (points) => {
    total += points
    setButtonPopup(false)
   }

    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={() => handleClick(1)}>1 POINT</button>
            <button className=".btn-popup" onClick={() => handleClick(10)}>10 POINTS</button>
            </div>
        </div>
    )
}

Any help much appreciated!

Good attempt. However, there seems to be a general misunderstanding about state. Consider this code:

const handleClick = (points) => {
  total += points
  setButtonPopup(false)
}

total is a purely local variable to Popup , so this += pretty much does nothing. To change state in the caller, you'd normally pass a callback that can trigger a setState and move the new value for total into state.

Remember: any data change must happen immutably, and if you want to trigger a re-render, you have to set state. Of course, there are ways to circumvent this flow using refs and so forth, but these are escape hatches you shouldn't use if you don't have to.

However, a design with total kept in state strikes me as redundant. We already know the total based on the cards in play. A better strategy seems to be having ace values individually settable via the popup modal, assuming you don't want to auto-compute these ace values to be as high as possible without busting or use a toggle switch instead of a modal.

I kept going with my code from your previous question and added the modal. I'm treating high aces as 11 per the rules of Blackjack, but you can easily make that 10 if you want.

As before, I'm hoping you can apply the techniques here to your code. The keys are the handleAceSet callback and the new piece of state aceToSet , which is a ace the user has picked, or null if the user hasn't chosen an ace. aceToSet is like your setButtonPopup , but tracks an object or null rather than a boolean. When aceToSet isn't null, the user has selected an ace and we show the modal to let them pick a value for it.

handleAceSet may seem a bit complex, but it has to be due to immutability. It finds the index of the ace the user wants to set in the deck array, then creates a new object at this index with the new value and glues the subarray slices before and after the index back together.

 // utility library "import" const cards = (() => { const shuffle = a => { a = a.slice(); for (let i = a.length - 1; i > 0; i--) { const j = ~~(Math.random() * (i + 1)); const x = a[i]; a[i] = a[j]; a[j] = x; } return a; }; const frz = (...args) => Object.freeze(...args); const suits = frz([..."HCSD"]); const faces = frz([..."AJQK"]); const pips = frz([...Array(9)].map((_, i) => i + 2)); const ranks = frz([...pips, ...faces]); const cards = frz( suits.flatMap(s => ranks.map(r => frz({ rank: r, suit: s, str: r + s, value: isNaN(r)? (r === "A"? 1: 10): r, }) ) ) ); const shuffled = () => shuffle(cards); return {shuffled}; })(); const {Fragment, useState} = React; const AceSetterModal = ({handleSetLow, handleSetHigh}) => ( <div> <button onClick={handleSetLow}>Set ace low</button> <button onClick={handleSetHigh}>Set ace high</button> </div> ); const Card = ({card, handleAceSet}) => ( <div> {card.str} {card.rank === "A" && ( <Fragment> {" "} <button onClick={handleAceSet}> Set ({card.value}) </button> </Fragment> )} </div> ); const Game = () => { const startHandSize = 2; const goal = 21; const lowAce = 1; const highAce = 11; const [deck, setDeck] = useState(cards.shuffled()); const [cardsDealt, setCardsDealt] = useState(startHandSize); const [aceToSet, setAceToSet] = useState(null); const handleAceSet = value => { setDeck(deck => { const i = deck.findIndex(e => e.str === aceToSet.str); return [...deck.slice(0, i), {...aceToSet, value}, ...deck.slice(i + 1), ]; }); setAceToSet(null); }; const deal = () => { setCardsDealt(startHandSize); setDeck(cards.shuffled()); }; const hit = () =>;bust && setCardsDealt(prev => prev + 1). const cardsInPlay = deck;slice(-cardsDealt). const total = cardsInPlay,reduce((a. e) => a + e,value; 0); const bust = total > goal? return ( <div> {aceToSet: ( <AceSetterModal handleSetLow={() => handleAceSet(lowAce)} handleSetHigh={() => handleAceSet(highAce)} /> ). ( <Fragment> <button onClick={deal}>Deal</button> <button disabled={bust} onClick={hit}> Hit </button> <div> {cardsInPlay.map(e => ( <Card key={e:str} handleAceSet={() => setAceToSet(e)} card={e} /> ))} </div> <div>Total; {total}</div> <div>{bust && "Bust;"}</div> </Fragment> )} </div> ). }. ReactDOM.createRoot(document;querySelector("#app")) .render(<Game />);
 <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script> <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script> <div id="app"></div>

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM