Build a Dynamic React JS Interactive Simple Memory Game

Ever found yourself captivated by the challenge and fun of a memory game? Those simple yet engaging games that test your recall and concentration. In this tutorial, we’re going to build our own version of this classic using React JS. This isn’t just about recreating a game; it’s about learning fundamental React concepts in a practical, hands-on way. We’ll cover components, state management, event handling, and conditional rendering. By the end, you’ll not only have a working memory game but also a solid understanding of how to build interactive web applications with React.

Why Build a Memory Game with React?

React is a powerful JavaScript library for building user interfaces. It’s component-based, making your code modular and reusable. React’s virtual DOM efficiently updates the UI, ensuring a smooth and responsive user experience. Building a memory game is an excellent way to learn React because it requires you to manage state, handle user interactions, and update the UI dynamically. It’s a project that’s challenging enough to teach you important concepts but simple enough to be completed without getting overwhelmed.

What We’ll Cover

  • Setting up a React project with Create React App.
  • Creating and managing component state.
  • Handling user interactions (clicking on cards).
  • Implementing game logic (matching cards, checking for a win).
  • Styling the game with basic CSS.

Prerequisites

Before we dive in, make sure you have the following:

  • Node.js and npm (or yarn) installed on your computer.
  • A basic understanding of HTML, CSS, and JavaScript.
  • A text editor or IDE (like VS Code) for writing code.

Step-by-Step Guide

1. Setting Up the Project

Let’s start by creating a new React project using Create React App. Open your terminal and run the following command:

npx create-react-app memory-game
cd memory-game

This will create a new React project named “memory-game”. Navigate into the project directory using the cd command.

2. Project Structure and Initial Setup

Inside your `memory-game` directory, you’ll find a structure similar to this:

memory-game/
├── node_modules/
├── public/
│   ├── index.html
│   └── ...
├── src/
│   ├── App.css
│   ├── App.js
│   ├── App.test.js
│   ├── index.css
│   ├── index.js
│   └── ...
├── package.json
└── ...

The main files we’ll be working with are in the `src` directory. Open `src/App.js` in your code editor and clear out the boilerplate code. We’ll start with a basic functional component.

import React from 'react';
import './App.css';

function App() {
  return (
    <div className="App">
      <h1>Memory Game</h1>
    </div>
  );
}

export default App;

This is the basic structure for our main App component. We’ve added a heading to indicate the game’s title. Let’s add some basic styling in `src/App.css`:

.App {
  text-align: center;
  font-family: sans-serif;
}

h1 {
  margin-top: 20px;
}

3. Creating the Card Component

A crucial part of our memory game is the card component. Create a new file named `src/Card.js` and add the following code:

import React from 'react';
import './Card.css';

function Card({ card, onClick, isFlipped, isMatched }) {
  return (
    <div
      className={`card ${isFlipped ? 'flipped' : ''} ${isMatched ? 'matched' : ''}`}
      onClick={() => onClick(card)}
    >
      <div className="card-inner">
        <div className="card-front">
          <img src="/question-mark.png" alt="Question Mark" />
        </div>
        <div className="card-back">
          {card.value}
        </div>
      </div>
    </div>
  );
}

export default Card;

In this component, we accept several props: card (the card’s data), onClick (a function to handle clicks), isFlipped (whether the card is face up), and isMatched (whether the card has been matched). The card’s appearance changes based on these props. We’ll also need some CSS for this component. Create `src/Card.css` and add:

.card {
  width: 100px;
  height: 100px;
  perspective: 1000px;
  margin: 10px;
  cursor: pointer;
}

.card-inner {
  position: relative;
  width: 100%;
  height: 100%;
  transition: transform 0.8s;
  transform-style: preserve-3d;
}

.card.flipped .card-inner {
  transform: rotateY(180deg);
}

.card.matched {
  opacity: 0.5;
  pointer-events: none;
}

.card-front, .card-back {
  position: absolute;
  width: 100%;
  height: 100%;
  backface-visibility: hidden;
  border-radius: 10px;
  box-shadow: 0 4px 8px rgba(0,0,0,0.2);
}

.card-front {
  background-color: #f0f0f0;
  display: flex;
  justify-content: center;
  align-items: center;
}

.card-back {
  background-color: #fff;
  transform: rotateY(180deg);
  display: flex;
  justify-content: center;
  align-items: center;
  font-size: 2em;
  font-weight: bold;
}

.card-front img {
  width: 70px;
  height: 70px;
}

This CSS sets up the basic look and feel of the card, including the flip animation. Make sure you have an image named `question-mark.png` in your public folder, or replace the `img src` with your chosen placeholder image.

4. Implementing the Game Logic in App.js

Now, let’s bring everything together in `src/App.js`. We’ll manage the game’s state and handle user interactions here. Update `src/App.js` with the following code:

import React, { useState, useEffect } from 'react';
import './App.css';
import Card from './Card';

function App() {
  const [cards, setCards] = useState([]);
  const [flippedCards, setFlippedCards] = useState([]);
  const [matchedCards, setMatchedCards] = useState([]);
  const [moves, setMoves] = useState(0);
  const [gameOver, setGameOver] = useState(false);

  // Array of card values
  const cardValues = [1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6];

  // Function to shuffle the cards
  const shuffleCards = () => {
    const shuffledCards = [...cardValues].sort(() => Math.random() - 0.5).map((value, index) => ({
      id: (index + 1),
      value,
      isFlipped: false,
      isMatched: false,
    }));
    setCards(shuffledCards);
  };

  // useEffect to initialize the game
  useEffect(() => {
    shuffleCards();
  }, []);

  // Handle card click
  const handleCardClick = (card) => {
    if (flippedCards.length  {
        if (c.id === card.id) {
          return { ...c, isFlipped: true };
        } else {
          return c;
        }
      });
      setCards(newCards);
      setFlippedCards([...flippedCards, card]);
    }
  };

  // useEffect to check for matches
  useEffect(() => {
    if (flippedCards.length === 2) {
      const [card1, card2] = flippedCards;
      if (card1.value === card2.value) {
        setMatchedCards([...matchedCards, card1.id, card2.id]);
        setFlippedCards([]);
      } else {
        setTimeout(() => {
          const newCards = cards.map(c => {
            if (c.id === card1.id || c.id === card2.id) {
              return { ...c, isFlipped: false };
            } else {
              return c;
            }
          });
          setCards(newCards);
          setFlippedCards([]);
        }, 1000);
      }
      setMoves(moves + 1);
    }
  }, [flippedCards, cards, matchedCards, moves]);

  // useEffect to check for game over
  useEffect(() => {
    if (matchedCards.length === cardValues.length) {
      setGameOver(true);
    }
  }, [matchedCards, cardValues.length]);

  // Restart the game
  const restartGame = () => {
    shuffleCards();
    setFlippedCards([]);
    setMatchedCards([]);
    setMoves(0);
    setGameOver(false);
  };

  return (
    <div className="App">
      <h1>Memory Game</h1>
      <p>Moves: {moves}</p>
      {gameOver && (
        <div className="game-over-message">
          <p>Congratulations! You won in {moves} moves!</p>
          <button onClick={restartGame}>Play Again</button>
        </div>
      )}
      <div className="card-grid">
        {cards.map(card => (
          <Card
            key={card.id}
            card={card}
            onClick={handleCardClick}
            isFlipped={card.isFlipped}
            isMatched={matchedCards.includes(card.id)}
          />
        ))}
      </div>
    </div>
  );
}

export default App;

Let’s break down what’s happening in this code:

  • State Variables: We use the useState hook to manage the game’s state:
    • cards: An array of card objects, each with an id, value, isFlipped, and isMatched property.
    • flippedCards: An array holding the currently flipped cards.
    • matchedCards: An array holding the IDs of the matched cards.
    • moves: Tracks the number of moves the player has made.
    • gameOver: A boolean indicating whether the game is over.
  • cardValues: An array containing the values for each card pair (e.g., [1, 2, 3, 4, 1, 2, 3, 4]).
  • shuffleCards(): This function shuffles the cardValues array and creates the initial card objects.
  • useEffect(() => { … }, []): This hook runs once after the component mounts, initializing the game by shuffling the cards.
  • handleCardClick(card): This function handles card clicks. It checks if fewer than two cards are flipped and if the clicked card isn’t already flipped or matched. It flips the selected card.
  • useEffect(() => { … }, [flippedCards, cards, matchedCards, moves]): This hook runs whenever flippedCards, cards, matchedCards, or moves changes. It checks if two cards are flipped. If they match, it marks them as matched. If they don’t match, it flips them back after a delay.
  • useEffect(() => { … }, [matchedCards, cardValues.length]): This hook checks if all cards are matched and sets gameOver to true.
  • restartGame(): Resets the game to its initial state.
  • Rendering Cards: The .map() function is used to render the card components. It passes the necessary props to each Card component, including the onClick handler, isFlipped, and isMatched properties.

Also, add the following to `src/App.css` to handle the grid layout and the game over message:

.card-grid {
  display: flex;
  flex-wrap: wrap;
  justify-content: center;
  width: 450px;
  margin: 0 auto;
}

.game-over-message {
  text-align: center;
  margin-top: 20px;
}

.game-over-message button {
  padding: 10px 20px;
  font-size: 1em;
  background-color: #4CAF50;
  color: white;
  border: none;
  border-radius: 5px;
  cursor: pointer;
}

5. Running the Application

Save all the files. Now, run your React application in the terminal:

npm start

This command will start the development server, and your memory game should open in your web browser (usually at http://localhost:3000). Play the game and test the functionality. You should be able to flip cards, match pairs, and see the game end when all cards are matched.

Common Mistakes and How to Fix Them

Here are some common mistakes and how to fix them when building a memory game with React:

  • Incorrect Card Matching Logic: Ensure your matching logic accurately compares card values. Double-check that you are comparing the correct properties of the card objects.
  • Incorrect State Updates: Make sure you’re updating the state correctly using setCards, setFlippedCards, and setMatchedCards. Incorrect state updates can lead to unexpected behavior and bugs. Use the spread operator (...) to create new arrays when updating state, which is crucial for React’s change detection.
  • Not Using Keys in .map(): When rendering a list of components (like our cards), always provide a unique key prop to each component. This helps React efficiently update the UI. In our code, we use key={card.id}.
  • Incorrect Event Handling: Ensure that the onClick handler is correctly attached to the card components and that it’s passing the correct card data.
  • Forgetting to Clear Flipped Cards: After a mismatch, you need to flip the cards back after a short delay. If you don’t clear the flippedCards array, the next click will cause unexpected behavior.
  • Incorrect Use of useEffect: The useEffect hook has specific rules for dependencies. Incorrect dependencies can lead to infinite loops or unexpected behavior. Review the dependencies array (the second argument of useEffect) carefully.

Key Takeaways

Let’s recap what we’ve learned:

  • Components: We created reusable Card components to represent each card in the game.
  • State Management: We used the useState hook to manage the game’s state, including card data, flipped cards, matched cards, moves, and game over status.
  • Event Handling: We used the onClick event to handle card clicks and trigger game logic.
  • Conditional Rendering: We used the isFlipped and isMatched props to conditionally render the card’s appearance.
  • useEffect Hook: We utilized the useEffect hook to handle side effects, such as shuffling the cards on game start and checking for matches.
  • Game Logic: We implemented the core game logic, including shuffling cards, flipping cards, matching pairs, and checking for a win.

FAQ

Here are some frequently asked questions about building a memory game with React:

  1. How can I add more cards to the game?
    To add more cards, simply increase the number of card values in the cardValues array in App.js. Make sure to include pairs of values to maintain the game’s matching functionality. You will also need to adjust the card grid’s width in the CSS to accommodate the increased number of cards.
  2. How can I make the game more visually appealing?
    You can enhance the game’s appearance by adding more CSS styling. Experiment with different card designs, background colors, fonts, and animations. Consider using images instead of simple text for the card values.
  3. How can I add a timer to the game?
    To add a timer, you can use the useState hook to manage the timer’s state (seconds elapsed) and the useEffect hook to start and stop the timer. Use setTimeout or setInterval to increment the timer. Remember to stop the timer when the game is over.
  4. How can I add a score?
    You can keep track of the score using the useState hook. The score can be incremented based on the number of matches or the time taken to complete the game. Display the score in the UI alongside the moves.
  5. How can I save the game’s high score?
    To save the high score, you can use local storage in the browser. Store the high score in local storage after each game. When the game loads, retrieve the high score from local storage and display it in the UI.

Building a memory game with React provides a great opportunity to explore React’s core concepts. You can customize this project further by adding more features. The journey of building the memory game can be a stepping stone for you to learn more about React and web development in general. With each feature, you deepen your understanding and become more proficient in React. Remember, the best way to learn is by doing, so keep experimenting, and happy coding!