Home > Software engineering >  How can I use setInterval to change state with or without useEffect?
How can I use setInterval to change state with or without useEffect?

Time:04-21

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:

  1. show is an array matching cards.length that keeps track of which cards have been flipped
  2. selected is an array that keeps track of every card a player has clicked

And two effects:

  1. When show cards changes, we determine if the player has won the game
  2. 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={["⚡️", "           
  • Related