#Whack A Mole#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.

By Pratik RaiMedium
Whack A Mole

Build a whack-a-mole game where moles randomly appear in holes and players must click them to score points. The key challenges are managing random mole appearances, timing mechanics, score tracking, and preventing double-click exploits. This game demonstrates effective use of setTimeout and React refs for game state management.

Overview

A whack-a-mole game displays a 3×3 grid of 9 holes. Moles randomly appear from these holes for 1.5 seconds, and players must click them before they disappear to score points. The game runs for 15 seconds, and players try to whack as many moles as possible.

Game Requirements

  • Grid: 9 holes arranged in a 3×3 grid
  • Mole appearance: One mole appears at a time, randomly selected from the 9 holes
  • Mole lifetime: Each mole stays visible for 1.5 seconds before disappearing
  • Scoring: Clicking a mole awards 1 point
  • Game duration: 15 seconds per game
  • Double-click prevention: Each mole can only be whacked once

Core Challenge: Recursive Mole Spawning

The most critical part is implementing a recursive spawning system where moles appear one at a time, and when a mole disappears (either by timeout or being whacked), a new mole immediately spawns.

Constants

The game uses constants for easy configuration:

TSXcomponent.tsx
1const TOTAL_TIME = 15; // Game duration in seconds 2const MOLE_LIFETIME = 1500; // How long mole stays visible (1.5 seconds) 3const GRID_SIZE = 9; // Total number of holes (3×3 grid)

Component Architecture

The implementation follows a recursive spawning pattern:

1. WhackAMole (Main Component)

Manages game state and recursive mole spawning:

TSXcomponent.tsx
1const WhackAMole = () => { 2 const [activeIndex, setActiveIndex] = useState<number | null>(null); 3 const [score, setScore] = useState(0); 4 const [timeLeft, setTimeLeft] = useState(TOTAL_TIME); 5 const [isPlaying, setIsPlaying] = useState(false); 6 7 const moleTimeoutRef = useRef<number | null>(null); 8 const gameTimerRef = useRef<number | null>(null); 9 const moleHitRef = useRef<boolean>(false); 10};

State Management:

  • activeIndex: The current hole index with an active mole (null if no mole)
  • score: Points accumulated by whacking moles
  • timeLeft: Remaining game time in seconds
  • isPlaying: Whether the game is currently active

Refs:

  • moleTimeoutRef: Stores the timeout ID for auto-hiding the mole
  • gameTimerRef: Stores the interval ID for the game countdown
  • moleHitRef: Prevents double-clicking the same mole

2. WhackAMoleComponent (Game Board)

Renders the 3×3 grid and displays moles:

TSXcomponent.tsx
1const WhackAMoleComponent = ({ 2 onWhack, 3 activeIndex 4}: { 5 onWhack: (index: number) => void; 6 activeIndex: number | null; 7}) => { 8 return ( 9 <div className="whack-a-mole"> 10 <div className="grid"> 11 {Array.from({ length: 9 }).map((_, index) => ( 12 <div 13 key={index} 14 className="hole" 15 onClick={() => onWhack(index)} 16 > 17 {activeIndex === index && "🐹"} 18 </div> 19 ))} 20 </div> 21 </div> 22 ); 23};

Responsibilities:

  • Renders 9 holes in a grid layout
  • Shows mole emoji (🐹) when activeIndex matches the hole index
  • Handles click events and passes the hole index to parent

Recursive Mole Spawning

The game uses a recursive spawning function instead of intervals:

TSXcomponent.tsx
1const spawnMole = () => { 2 moleHitRef.current = false; // Reset hit flag for new mole 3 const index = getRandomIndex(); 4 setActiveIndex(index); 5 6 moleTimeoutRef.current = window.setTimeout(() => { 7 setActiveIndex(null); 8 9 if (isPlaying) { 10 spawnMole(); // Recursively spawn next mole 11 } 12 }, MOLE_LIFETIME); 13};

How it works:

  1. Reset the moleHitRef flag so the new mole can be whacked
  2. Generate a random hole index (0-8)
  3. Set that hole as active
  4. Set a timeout for MOLE_LIFETIME (1.5 seconds)
  5. When timeout fires:
    • Clear the active mole
    • If game is still playing, recursively call spawnMole() again

Advantages of recursive spawning:

  • Continuous flow: New mole appears immediately after previous one disappears
  • No gaps: No delay between moles (unlike fixed intervals)
  • Self-managing: Each mole manages its own lifecycle
  • Clean: Automatically stops when game ends

Random Index Generation

TSXcomponent.tsx
1const getRandomIndex = () => Math.floor(Math.random() * GRID_SIZE);

This generates a random integer from 0 to 8, corresponding to one of the 9 holes.

Preventing Double-Clicks

The game prevents double-click exploits using a ref:

TSXcomponent.tsx
1const handleWhack = (index: number) => { 2 if (!isPlaying || index !== activeIndex) return; 3 if (moleHitRef.current) return; // Already whacked this mole 4 5 moleHitRef.current = true; // Mark as whacked 6 7 setScore(prev => prev + 1); 8 9 // Clear the auto-hide timeout 10 if (moleTimeoutRef.current) { 11 clearTimeout(moleTimeoutRef.current); 12 } 13 14 setActiveIndex(null); 15 spawnMole(); // Immediately spawn next mole 16};

Double-click prevention:

  1. Check if game is playing and clicked hole has active mole
  2. Check moleHitRef.current - if true, mole already whacked, ignore click
  3. Set moleHitRef.current = true immediately to prevent subsequent clicks
  4. Increment score
  5. Clear the timeout (mole was whacked, no need to auto-hide)
  6. Clear active mole and spawn next one

Why this works:

  • moleHitRef is a ref, so it updates synchronously
  • Even if user clicks rapidly, second click sees moleHitRef.current === true and returns early
  • Each new mole resets the flag in spawnMole()

Game Timer

The game timer runs independently from mole spawning:

TSXcomponent.tsx
1gameTimerRef.current = window.setInterval(() => { 2 setTimeLeft(prev => { 3 if (prev <= 1) { 4 stopGame(); 5 return 0; 6 } 7 return prev - 1; 8 }); 9}, 1000);

Features:

  • Decrements every second
  • Automatically stops game when timer reaches 0
  • Runs independently of mole spawning logic

Starting and Stopping the Game

Start Game

TSXcomponent.tsx
1const startGame = () => { 2 stopGame(); // Clean up any existing timers 3 4 setScore(0); 5 setTimeLeft(TOTAL_TIME); 6 setIsPlaying(true); 7 8 spawnMole(); // Start the mole spawning chain 9 10 gameTimerRef.current = window.setInterval(() => { 11 setTimeLeft(prev => { 12 if (prev <= 1) { 13 stopGame(); 14 return 0; 15 } 16 return prev - 1; 17 }); 18 }, 1000); 19};

Flow:

  1. Clean up any existing game state
  2. Reset score and timer
  3. Set playing state to true
  4. Start recursive mole spawning
  5. Start game countdown timer

Stop Game

TSXcomponent.tsx
1const stopGame = () => { 2 if (moleTimeoutRef.current) clearTimeout(moleTimeoutRef.current); 3 if (gameTimerRef.current) clearInterval(gameTimerRef.current); 4 5 setActiveIndex(null); 6 setIsPlaying(false); 7};

Cleanup:

  • Clear mole timeout (stops recursive spawning)
  • Clear game timer (stops countdown)
  • Clear active mole
  • Set playing state to false

Cleanup on Unmount

Always clean up timers when component unmounts:

TSXcomponent.tsx
1useEffect(() => { 2 return () => { 3 if (moleTimeoutRef.current) clearTimeout(moleTimeoutRef.current); 4 if (gameTimerRef.current) clearInterval(gameTimerRef.current); 5 }; 6}, []);

This prevents memory leaks and ensures timers don't continue running after the component is removed.

Styling the Game

Grid Layout

The game uses CSS Grid for the 3×3 layout:

CSSstyles.css
1.grid { 2 display: grid; 3 grid-template-columns: repeat(3, 1fr); 4 gap: 10px; 5} 6 7.hole { 8 width: 100px; 9 height: 100px; 10 aspect-ratio: 1; 11 background-color: #8b4513; 12 border-radius: 50%; 13 cursor: pointer; 14 display: flex; 15 align-items: center; 16 justify-content: center; 17}

Features:

  • 3 columns, equal width (repeat(3, 1fr))
  • Circular holes with brown background
  • Centered content (mole emoji)
  • Pointer cursor for interactivity

Container Styling

CSSstyles.css
1.whack-a-mole-container { 2 display: flex; 3 flex-direction: column; 4 align-items: center; 5 justify-content: center; 6 gap: 20px; 7 padding: 20px; 8 background-color: #1a1a1a; 9 border-radius: 10px; 10 max-width: 600px; 11 margin: 0 auto; 12}

Key Takeaways

  1. Recursive Spawning: Use recursive function calls instead of intervals for continuous mole flow
  2. Refs for Synchronous Checks: Use refs to prevent double-clicks (refs update synchronously)
  3. Separate Timers: Game timer and mole spawning are independent systems
  4. Proper Cleanup: Always clear timeouts and intervals to prevent memory leaks
  5. Single Active Mole: Only one mole active at a time (simpler state management)
  6. Immediate Feedback: Spawn next mole immediately after whacking or timeout
  7. Constants for Configuration: Use constants for easy game tuning

The beauty of this implementation is its simplicity. The recursive spawning pattern creates a smooth, continuous flow of moles without gaps. The ref-based double-click prevention is elegant and effective. The separation of concerns (spawning vs. timing) makes the code maintainable and easy to understand.

Goal: Implement a whack-a-mole game component with scoring and timing mechanics.

Frequently asked questions

What is whack-a-mole really testing?
Intervals and stale closures. An interval callback closes over the state from the render that created it, so reading the timer value inside it reads the same number forever and the countdown freezes. The updater form — `setTimeLeft(s => s - 1)` — asks React for the current value instead of remembering an old one.
Why does every interval need a cleanup?
Because pressing Start twice otherwise leaves two intervals running, and the mole moves at double speed for reasons nobody can see. Every effect that starts an interval must return the `clearInterval`, or a round that ends leaves its timers firing against a component that has moved on.
How do you stop the same mole being scored twice?
With a ref rather than state. Two rapid clicks are processed before React re-renders, so both see the mole unchanged and both score. A ref is written and read synchronously, so the second click sees the first immediately. Knowing when a ref beats state is the point of the question.
What are the follow-ups?
Difficulty that ramps through the round, a persisted high score, and — the interesting one — pausing when the tab is hidden, which brings in the Page Visibility API and the fact that browsers throttle background timers anyway.

Related Challenges

Continue learning with these related challenges

View All
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.

React · JavaScriptPratik 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 · JavaScriptPratik 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 · JavaScriptPratik Rai ·