#Memory Game#React

Memory Game

Create a card-matching memory game in React with flip animations, pair detection logic, move counter, timer, and win condition handling. A classic frontend coding challenge.

By Pratik RaiMedium
Memory Game

Build a memory game where players flip cards to find matching pairs. The key challenges are managing card state (flipped, matched), preventing multiple card selections, handling match logic, and tracking game progress. This classic game is a great way to practice React state management and event handling.

Overview

A memory game displays a 4ร—4 grid of 16 cards (8 pairs of emojis). Players click cards to flip them, trying to find matching pairs. When two cards are flipped, they're compared. If they match, they stay visible. If not, they flip back after a short delay. The game tracks moves and ends when all pairs are matched.

Game Setup

Card Generation

The game starts with a pool of emoji options and creates pairs:

TSXcomponent.tsx
1const emojis = [ 2 '๐Ÿต', '๐Ÿถ', '๐ŸฆŠ', '๐Ÿฑ', '๐Ÿฆ', '๐Ÿฏ', '๐Ÿด', '๐Ÿฆ„', 3 '๐Ÿฆ“', '๐ŸฆŒ', '๐Ÿฎ', '๐Ÿท', '๐Ÿญ', '๐Ÿน', '๐Ÿป', 4 '๐Ÿจ', '๐Ÿผ', '๐Ÿฝ', '๐Ÿธ', '๐Ÿฐ', '๐Ÿ™', 5]; 6 7const getShuffledCards = () => { 8 const selected = emojis.slice(0, 8); // Select first 8 emojis 9 const pairs = [...selected, ...selected]; // Create pairs (16 cards) 10 11 return pairs.map((item, index) => ({ 12 id: index, 13 value: item, 14 isFlipped: false, 15 isMatched: false 16 })).sort(() => Math.random() - 0.5); // Shuffle randomly 17};

How it works:

  1. Select first 8 emojis from the pool
  2. Duplicate them to create 8 pairs (16 cards total)
  3. Map each emoji to a card object with:
    • id: Unique identifier (0-15)
    • value: The emoji character
    • isFlipped: Whether card is currently face-up
    • isMatched: Whether card has been matched with its pair
  4. Shuffle using Math.random() - 0.5 to randomize order

Shuffling explanation:

Component Architecture

The implementation uses a parent-child component pattern:

1. MemoryGame (Main Component)

Manages game state and logic:

TSXcomponent.tsx
1const MemoryGame = () => { 2 const [cards, setCards] = useState(getShuffledCards()); 3 const [selectedIndex, setSelectedIndex] = useState<any>([]); 4 const [lockBoard, setLockBoard] = useState(false); 5 const [moves, setMoves] = useState(0); 6 const [gameOver, setGameOver] = useState(false); 7};

State Management:

  • cards: Array of 16 card objects with their current state
  • selectedIndex: Array tracking currently selected card indices (max 2)
  • lockBoard: Boolean preventing clicks during flip-back animation
  • moves: Counter for number of card pair attempts
  • gameOver: Boolean indicating if all pairs are matched

2. MemoryGameComponent (Card Grid)

Renders the card grid and handles clicks:

TSXcomponent.tsx
1const MemoryGameComponent = ({ onCardClick, cards }) => { 2 return ( 3 <div className="memory-game"> 4 <div className="cards"> 5 {cards.map((card, index) => ( 6 <div 7 className={`card ${card.isMatched ? "matched" : card.isFlipped ? "flipped" : ""}`} 8 key={card.id} 9 onClick={() => onCardClick(index)} 10 > 11 {card.isFlipped || card.isMatched ? card.value : ""} 12 </div> 13 ))} 14 </div> 15 </div> 16 ); 17};

Responsibilities:

  • Renders 16 cards in a 4ร—4 grid
  • Applies CSS classes based on card state
  • Shows emoji when card is flipped or matched
  • Handles click events

Card Click Handling

The core logic handles card flips and match checking:

TSXcomponent.tsx
1const handleCardClick = (id: number) => { 2 if (lockBoard) return; // Prevent clicks during animation 3 4 const card = cards[id]; 5 6 if (card.isFlipped || card.isMatched) return; // Ignore already flipped/matched cards 7 8 // Flip the card 9 const newCards = [...cards]; 10 newCards[id] = { ...card, isFlipped: true }; 11 setCards(newCards); 12 13 // Add to selected cards 14 const newSelected = [...selectedIndex, id]; 15 setSelectedIndex(newSelected); 16 17 // If 2 cards selected, check for match 18 if (newSelected.length === 2) { 19 setMoves(prev => prev + 1); 20 checkMatch(newSelected, newCards); 21 } 22};

Flow:

  1. Guard checks: Return early if board is locked or card already flipped/matched
  2. Flip card: Create new array, update clicked card's isFlipped to true
  3. Track selection: Add card index to selectedIndex array
  4. Check match: When 2 cards selected, increment moves and check for match

Match Checking Logic

When two cards are selected, the game checks if they match:

TSXcomponent.tsx
1const checkMatch = (selectedIndexes: any, updatedCard: any[]) => { 2 const [first, second] = selectedIndexes; 3 4 // If it's a match 5 if (updatedCard[first].value === updatedCard[second].value) { 6 const matchedCards = [...updatedCard]; 7 8 matchedCards[first].isMatched = true; 9 matchedCards[second].isMatched = true; 10 11 setCards(matchedCards); 12 setSelectedIndex([]); 13 checkGameOver(matchedCards); 14 } 15 // If not a match 16 else { 17 setLockBoard(true); // Lock board during flip-back 18 19 setTimeout(() => { 20 const resetCards = [...cards]; // Use current state 21 22 resetCards[first].isFlipped = false; 23 resetCards[second].isFlipped = false; 24 25 setSelectedIndex([]); 26 setCards(resetCards); 27 setLockBoard(false); 28 }, 800); // 800ms delay before flipping back 29 } 30};

Match Found:

  1. Mark both cards as isMatched: true
  2. Clear selected cards array
  3. Check if game is over

No Match:

  1. Lock the board (prevent new clicks)
  2. Wait 800ms for players to see the cards
  3. Flip both cards back (isFlipped: false)
  4. Clear selection and unlock board

Important Note: The resetCards uses [...cards] (current state), not updatedCard, because state updates are asynchronous. This ensures we're working with the latest state.

Game Over Detection

The game ends when all cards are matched:

TSXcomponent.tsx
1const checkGameOver = (cardList: any[]) => { 2 const allMatched = cardList.every(card => card.isMatched); 3 4 if (allMatched) { 5 setGameOver(true); 6 } 7};

How it works:

  • every() checks if all cards have isMatched === true
  • If true, set gameOver to trigger "Play Again" button

Board Locking

The lockBoard state prevents clicks during the flip-back animation:

TSXcomponent.tsx
1if (lockBoard) return; // In handleCardClick

Why it's needed:

  • Without locking, players could click more cards during the 800ms delay
  • This would break the "two cards at a time" rule
  • Locking ensures players wait for cards to flip back before continuing

Card State Priority

The card styling uses a priority system:

TSXcomponent.tsx
1className={`card ${card.isMatched ? "matched" : card.isFlipped ? "flipped" : ""}`}

Priority order:

  1. Matched: If isMatched === true, apply "matched" class (green border)
  2. Flipped: If not matched but isFlipped === true, apply "flipped" class (gray background)
  3. Default: Otherwise, no special class (dark background)

Visual states:

  • Default: Dark background (#333), no emoji visible
  • Flipped: Gray background (#666), emoji visible
  • Matched: Transparent background with green border, emoji always visible

Display Logic

Cards show their emoji value conditionally:

TSXcomponent.tsx
1{card.isFlipped || card.isMatched ? card.value : ""}

Display rules:

  • Show emoji if card is flipped OR matched
  • Hide emoji if card is face-down and not matched
  • Matched cards always show emoji (even if isFlipped becomes false)

Game Reset

Players can restart the game:

TSXcomponent.tsx
1const resetGame = () => { 2 setCards(getShuffledCards()); // New random shuffle 3 setSelectedIndex([]); 4 setMoves(0); 5 setGameOver(false); 6 setLockBoard(false); 7};

Reset actions:

  • Generate new shuffled cards
  • Clear selected cards
  • Reset move counter
  • Reset game over state
  • Unlock board

Styling

Grid Layout

The game uses CSS Grid for the 4ร—4 card layout:

CSSstyles.css
1.cards { 2 display: grid; 3 grid-template-columns: repeat(4, 1fr); 4 gap: 10px; 5}

Features:

  • 4 equal-width columns
  • 10px gap between cards
  • Responsive grid that adapts to container size

Card States

CSSstyles.css
1.card { 2 width: 60px; 3 height: 60px; 4 background-color: #333; 5 border-radius: 10px; 6 display: flex; 7 align-items: center; 8 justify-content: center; 9} 10 11.card.flipped { 12 background-color: #666; 13} 14 15.card.matched { 16 background-color: transparent; 17 border: 1px solid green; 18}

Visual feedback:

  • Default: Dark card, no content visible
  • Flipped: Lighter background, emoji visible
  • Matched: Transparent with green border, emoji always visible

Key Takeaways

  1. State Management: Track isFlipped and isMatched separately for different behaviors
  2. Board Locking: Use lockBoard to prevent clicks during animations
  3. Selection Tracking: Use array to track selected card indices (max 2)
  4. Match Logic: Compare card values when 2 cards are selected
  5. Delayed Flip-Back: Use setTimeout to show mismatched cards before flipping
  6. State Priority: Matched cards take priority over flipped cards in styling
  7. Move Tracking: Increment moves only when checking a pair (not on every click)
  8. Game Over: Check if all cards are matched after each successful match

The beauty of this implementation is its clear separation of concerns. The card state (isFlipped, isMatched) drives both the visual appearance and the game logic. The board locking mechanism ensures a smooth user experience during animations. The move counter provides feedback on player performance, making the game more engaging.

Goal: Implement a memory game component with card matching and scoring mechanics.

Frequently asked questions

Where does the comparison logic belong?
In an effect, not the click handler. At the moment of the second click, state still holds the previous value โ€” the second card has not been recorded yet, so comparing there reads stale data. An effect keyed on the flipped array runs after React has rendered the second card, which is also what lets the player see it before it flips back.
Why does the timer need a cleanup?
Because the player can press "New game" during the pause. Without `clearTimeout` in the effect's cleanup, the pending callback still fires against the fresh deck and flips away cards nobody touched.
How do you stop a fast player flipping the whole board?
Three guards at the top of the handler: already matched, already face up, and two cards already being compared. The third is the one that matters โ€” without it, rapid clicking flips every card before the first comparison resolves.
Why track matched pairs by symbol rather than by card id?
Because a pair matches together, and there is no state where one half is matched and the other is not. It also turns the win condition into a size comparison rather than a division. A small choice, but interviewers notice when the data model matches the rules of the game.

Related Challenges

Continue learning with these related challenges

View All
React

Whack A Mole

Build a fun Whack-a-Mole game in React with random mole spawning, click detection, score tracking, countdown timer, and increasing difficulty levels. Perfect for React practice.

React ยท JavaScript โ€” Pratik Rai ยท

React

Simon Says

Build the classic Simon Says memory game in React with color sequence generation, player input validation, sound effects, increasing difficulty, and high score tracking.

React ยท JavaScript โ€” Pratik Rai ยท

React

Image Carousel

Create an interactive image carousel in React with smooth slide transitions, navigation arrows, dot indicators, autoplay, and touch/swipe support for mobile devices.

React ยท JavaScript โ€” Pratik Rai ยท