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
useStatehook to manage the game’s state: cards: An array of card objects, each with anid,value,isFlipped, andisMatchedproperty.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
cardValuesarray 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, ormoveschanges. 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
gameOvertotrue. - 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 eachCardcomponent, including theonClickhandler,isFlipped, andisMatchedproperties.
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, andsetMatchedCards. 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 uniquekeyprop to each component. This helps React efficiently update the UI. In our code, we usekey={card.id}. - Incorrect Event Handling: Ensure that the
onClickhandler 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
flippedCardsarray, the next click will cause unexpected behavior. - Incorrect Use of
useEffect: TheuseEffecthook has specific rules for dependencies. Incorrect dependencies can lead to infinite loops or unexpected behavior. Review the dependencies array (the second argument ofuseEffect) carefully.
Key Takeaways
Let’s recap what we’ve learned:
- Components: We created reusable
Cardcomponents to represent each card in the game. - State Management: We used the
useStatehook to manage the game’s state, including card data, flipped cards, matched cards, moves, and game over status. - Event Handling: We used the
onClickevent to handle card clicks and trigger game logic. - Conditional Rendering: We used the
isFlippedandisMatchedprops to conditionally render the card’s appearance. - useEffect Hook: We utilized the
useEffecthook 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:
- How can I add more cards to the game?
To add more cards, simply increase the number of card values in thecardValuesarray inApp.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. - 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. - How can I add a timer to the game?
To add a timer, you can use theuseStatehook to manage the timer’s state (seconds elapsed) and theuseEffecthook to start and stop the timer. UsesetTimeoutorsetIntervalto increment the timer. Remember to stop the timer when the game is over. - How can I add a score?
You can keep track of the score using theuseStatehook. 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. - 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!
