I am doing a memory game in which every time the user clicks on a card it executes flipcard passing the card id to it, and changes the flip status of that card, updating the gameState.
After a card is flipped, I want to execute checkMatchingCards to check if the flipped cards are of the same type, if they aren't it must flip the cards back, updating gameState, BUT after a second, not instantly.
What is the best way to assure that both functions are executed in that order, flipcard and then checkMatchingCards with the last one waiting a second?
const flipCard = (id) => {
if (!canFlipCard(id)) return
// Find card to flip
const cardToFlip = gameState.cards.find(card => card.id === id)
cardToFlip.isFlipped = !cardToFlip.isFlipped
setGameState({
...gameState,
cards: gameState.cards.map(card => (card.id === cardToFlip.id ? cardToFlip : card)),
flippedCards: gameState.flippedCards.concat(cardToFlip)
})
}
const checkMatchingCards = (cards) => {
if (!cards) return
const cardsMatch = cards.every(card => card.color === cards[0].color)
// If cards match and reach number of cards to match
if (cardsMatch && cards.length === MATCH_GOAL) {
setGameState({
...gameState,
flippedCards: [],
matchedCards: gameState.matchedCards.concat(cards),
})
}
// If cards doesn't match, clean flippedCards
else if (!cardsMatch && cards.length >= 2) {
setGameState({
...gameState,
cards: gameState.cards.map(card => gameState.flippedCards.includes(card, 0) ? { ...card, isFlipped: false } : card),
flippedCards: [],
})
}
}
I found two ways of doing this, one is using setTimeout so every time the game renders, it checks for matching cards,
setTimeout(() => {
checkMatchingCards(gameState.flippedCards)
}, 900)
And another way with useEffect and setTImeout, which executes only when flippedCards change:
useEffect(()=> {
setTimeout(() => {
checkMatchingCards(gameState.flippedCards)
}, 900)
}, gameState.flippedCards)
I leave the entire code here, in case it helps https://github.com/takihama/memory-game
CodePudding user response:
You should use useEffect
in this case.
Don't call setTimeout
outside of useEffect
, this will be called every time the component re-render (which isn't necessary since checkMatchingCards
only depends on gameState.flippedCards
).
And you should passuseEffect
dependencies as an array
useEffect(()=> {
setTimeout(() => {
checkMatchingCards(gameState.flippedCards)
}, 900)
}, [gameState.flippedCards])
CodePudding user response:
Here's a minimal example that uses two states:
show
is an array matchingcards.length
that keeps track of which cards have been flippedselected
is an array that keeps track of every card a player has clicked
And two effects:
- When
show
cards changes, we determine if the player has won the game - When
selected
cards changes, if there is an even number of cards, a pair is compared. If they do not match,setTimeout
hides them again
function App({ cards = [] }) {
// state for hidden/flipped cards and player selection
const [show, setShow] = React.useState(Array(cards.length).fill(false))
const [selected, setSelected] = React.useState([])
// helper for toggling card state
const toggle = key => setShow(s => updateArray(s, key, v => !v))
// when shown cards change, determine if the player won the game
React.useEffect(_ => {
if (show.every(Boolean)) console.log(`you won in ${selected.length/2} guesses`)
}, [show])
// when selected cards change, determine whether they should stay flipped
React.useEffect(_ => {
if (selected.length & 1 || cards[selected[0]] == cards[selected[1]]) return
setTimeout(_ => { toggle(selected[0]), toggle(selected[1]) }, 1000)
}, [selected])
// when the player clicks a hidden card, flip it and add it to the selection
const onClick = key => event => {
if (show[key]) return
toggle(key), setSelected(s => [key, ...s])
}
// render cards
return cards.map((c, key) =>
<Card key={key} text={c} onClick={onClick(key)} show={show[key]} />
)
}
Depends on a generic updateArray
helper for immutably updating arrays:
function updateArray(a, key, func) {
return [...a.slice(0, key), func(a[key]), ...a.slice(key 1)]
}
And a stateless Card
component:
const Card = ({ text, onClick, show }) =>
<button type="button" onClick={onClick} children={show ? text : "?" } />
Run the demo below and try to win the game!
function App({ cards = [] }) {
const [show, setShow] = React.useState(Array(cards.length).fill(false))
const [selected, setSelected] = React.useState([])
const toggle = key => setShow(s => updateArray(s, key, v => !v))
React.useEffect(_ => {
if (show.every(Boolean)) console.log(`you won in ${selected.length/2} guesses`)
}, [show])
React.useEffect(_ => {
if (selected.length & 1 || cards[selected[0]] == cards[selected[1]]) return
setTimeout(_ => { toggle(selected[0]), toggle(selected[1]) }, 1000)
}, [selected])
const onClick = key => event => {
if (show[key]) return
toggle(key), setSelected(s => [key, ...s])
}
return cards.map((c, key) =>
<Card key={key} text={c} onClick={onClick(key)} show={show[key]} />
)
}
function updateArray(a, key, func) {
return [...a.slice(0, key), func(a[key]), ...a.slice(key 1)]
}
const Card = ({ text, onClick, show }) =>
<button type="button" onClick={onClick} children={show ? text : "?" } />
ReactDOM.render(
<App cards={["⚡️", "