In the digital age, where attention spans are constantly shrinking, interactive and engaging web applications are more crucial than ever. Building a fun, interactive memory game is a fantastic way to learn Next.js, a powerful React framework for building modern web applications. This project allows you to grasp fundamental concepts like component creation, state management, event handling, and conditional rendering, all while creating something enjoyable. This guide will walk you through the process, providing clear explanations, step-by-step instructions, and helpful tips to ensure your success.
Why Build a Memory Game with Next.js?
Next.js offers several advantages for this project and for web development in general:
- Server-Side Rendering (SSR) and Static Site Generation (SSG): This improves SEO and initial load times, making your game more accessible and performant.
- Routing: Next.js simplifies routing, making it easy to manage different game states or add additional features like a leaderboard later.
- API Routes: Easily create backend functionality if you decide to store high scores or implement user accounts.
- Developer Experience: Features like hot reloading and built-in CSS support streamline the development process.
Building a memory game provides a practical application for learning these Next.js features, solidifying your understanding of web development principles.
Project Overview: What We’ll Build
Our memory game will feature a grid of cards with hidden images. Players will flip over cards to find matching pairs. The game will track the number of moves and the player’s score. Here’s a breakdown of the core features:
- A grid of cards.
- Hidden images on the cards.
- Ability to flip cards to reveal images.
- Logic to detect matching pairs.
- A moves counter.
- Game over condition and a way to restart the game.
Step-by-Step Guide
1. Setting Up Your Next.js Project
If you don’t have Node.js and npm (or yarn) installed, you’ll need to do that first. Then, open your terminal and run the following commands to create a new Next.js project:
npx create-next-app memory-game
cd memory-game
This will create a new directory named “memory-game” with the basic Next.js project structure. You can choose to use TypeScript or JavaScript; this guide will use JavaScript. Next, start the development server:
npm run dev
Open your browser and navigate to http://localhost:3000. You should see the default Next.js welcome page.
2. Project Structure and File Setup
Inside your `memory-game` directory, you’ll see a structure like this:
memory-game/
├── node_modules/
├── pages/
│ └── _app.js
│ └── index.js
├── public/
├── styles/
│ └── globals.css
│ └── Home.module.css
├── .gitignore
├── next.config.js
├── package-lock.json
├── package.json
└── README.md
We’ll primarily be working within the `pages` directory. Let’s create the following files:
- `pages/index.js`: This will be our main game component.
- `components/Card.js`: This will represent an individual card. Create a folder named `components` in the root directory.
- `styles/MemoryGame.module.css`: This will hold our game-specific styles.
3. Building the Card Component (`components/Card.js`)
This component will handle the visual representation of a single card and its flipping behavior. Here’s the code for `components/Card.js`:
import styles from '../styles/MemoryGame.module.css';
const Card = ({ card, onClick, isFlipped, isDisabled }) => {
return (
<div> !isDisabled && onClick(card)}
>
<div>
<div>
?
</div>
<div>
<img src="{card.image}" alt="card image" />
</div>
</div>
</div>
);
};
export default Card;
Let’s break down the code:
- Import styles: We import our CSS module for styling.
- Props: The component receives several props:
- `card`: An object containing information about the card (e.g., image, id).
- `onClick`: A function to handle the card click.
- `isFlipped`: A boolean indicating if the card is flipped.
- `isDisabled`: A boolean indicating if the card should be disabled (e.g., during a match check).
- JSX Structure:
- The main `div` with classes for styling and controlling the flip state. The `isDisabled` prop is used to prevent clicks during certain game states.
- `cardInner`: This div acts as a container for the front and back of the card, allowing the flip animation.
- `cardFront`: Displays the question mark.
- `cardBack`: Displays the image of the card, when flipped.
- onClick Handler: The `onClick` function is triggered when the card is clicked, but only if it’s not disabled.
Now, create the `MemoryGame.module.css` file in the `styles` directory and add the following CSS:
.card {
width: 100px;
height: 100px;
perspective: 1000px;
margin: 10px;
cursor: pointer;
border-radius: 5px;
transition: transform 0.6s;
position: relative;
}
.card.flipped {
transform: rotateY(180deg);
}
.card.disabled {
pointer-events: none;
opacity: 0.6;
}
.cardInner {
width: 100%;
height: 100%;
position: relative;
transform-style: preserve-3d;
transition: transform 0.6s;
}
.cardFront, .cardBack {
position: absolute;
width: 100%;
height: 100%;
backface-visibility: hidden;
border-radius: 5px;
border: 1px solid #ccc;
}
.cardFront {
background-color: #f0f0f0;
font-size: 4em;
text-align: center;
line-height: 100px;
}
.cardBack {
background-color: #fff;
transform: rotateY(180deg);
}
.cardBack img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 5px;
}
This CSS provides the basic styling for the cards, including the flip animation.
4. Building the Main Game Component (`pages/index.js`)
This is where the main game logic resides. Here’s the code for `pages/index.js`:
import { useState, useEffect } from 'react';
import Card from '../components/Card';
import styles from '../styles/MemoryGame.module.css';
const images = [
'/images/image1.jpg',
'/images/image2.jpg',
'/images/image3.jpg',
'/images/image4.jpg',
'/images/image5.jpg',
'/images/image6.jpg',
];
const generateCards = () => {
const cards = [];
const cardImages = [...images, ...images]; // Duplicate images for pairs
cardImages.forEach((image, index) => {
cards.push({
id: index,
image: image,
flipped: false,
matched: false,
});
});
// Shuffle the cards
cards.sort(() => Math.random() - 0.5);
return cards;
};
const MemoryGame = () => {
const [cards, setCards] = useState(generateCards());
const [selectedCards, setSelectedCards] = useState([]);
const [moves, setMoves] = useState(0);
const [gameWon, setGameWon] = useState(false);
const [isDisabled, setIsDisabled] = useState(false);
useEffect(() => {
if (selectedCards.length === 2) {
setIsDisabled(true);
const [firstCard, secondCard] = selectedCards;
if (cards[firstCard.id].image === cards[secondCard.id].image) {
// Match
setCards(prevCards => {
return prevCards.map(card => {
if (card.id === firstCard.id || card.id === secondCard.id) {
return { ...card, matched: true };
}
return card;
});
});
setSelectedCards([]);
setIsDisabled(false);
} else {
// No match
setTimeout(() => {
setCards(prevCards => {
return prevCards.map(card => {
if (card.id === firstCard.id || card.id === secondCard.id) {
return { ...card, flipped: false };
}
return card;
});
});
setSelectedCards([]);
setIsDisabled(false);
}, 1000);
}
setMoves(moves => moves + 1);
}
}, [selectedCards, cards]);
useEffect(() => {
if (cards.every(card => card.matched)) {
setGameWon(true);
}
}, [cards]);
const handleCardClick = (card) => {
if (isDisabled || card.matched || selectedCards.includes(card)) return;
setCards(prevCards =>
prevCards.map(c => (c.id === card.id ? { ...c, flipped: true } : c))
);
setSelectedCards(prev => [...prev, card]);
};
const resetGame = () => {
setCards(generateCards());
setSelectedCards([]);
setMoves(0);
setGameWon(false);
};
return (
<div>
<h1>Memory Game</h1>
<p>Moves: {moves}</p>
{gameWon && <p>Congratulations! You won!</p>}
<div>
{cards.map(card => (
))}
</div>
{gameWon && (
<button>Play Again</button>
)}
</div>
);
};
export default MemoryGame;
Let’s break down this code:
- Imports: We import `useState` and `useEffect` from React, and the `Card` component and our CSS module.
- Image Array: `images` array stores the paths to the images. (You’ll need to add your images to a folder, e.g., `/public/images/`, and update the paths accordingly).
- `generateCards()` Function: This function creates an array of card objects. It duplicates the images to create pairs, adds `id`, `flipped`, and `matched` properties, and then shuffles the cards using `sort(() => Math.random() – 0.5)`.
- State Variables:
- `cards`: An array of card objects, representing the game state.
- `selectedCards`: An array to hold the currently selected cards (up to two).
- `moves`: The number of moves the player has made.
- `gameWon`: A boolean indicating if the game has been won.
- `isDisabled`: A boolean to prevent user interaction during certain states (e.g., when checking for a match).
- `useEffect` Hooks:
- The first `useEffect` hook runs whenever `selectedCards` or `cards` changes. It checks if two cards are selected. If so, it disables user interaction (`setIsDisabled(true)`) and checks for a match.
- Match: If the cards match, it updates the `cards` state to mark the cards as `matched` and resets `selectedCards`.
- No Match: If the cards don’t match, it flips the cards back over after a short delay (1 second) and resets `selectedCards`.
- The second `useEffect` hook runs whenever `cards` changes. It checks if all cards are matched. If so, it sets `gameWon` to `true`.
- `handleCardClick()` Function: This function is called when a card is clicked. It checks for various conditions (e.g., if the game is disabled, if the card is already matched) and then flips the card by updating the `cards` state. It also adds the card to the `selectedCards` array.
- `resetGame()` Function: Resets the game to its initial state.
- JSX Structure:
- Displays the game title, the number of moves, and a winning message.
- Maps over the `cards` array and renders a `Card` component for each card.
- Passes the necessary props to the `Card` component: `card`, `onClick`, `isFlipped`, and `isDisabled`.
- Provides a “Play Again” button when the game is won.
Now, let’s add the game-specific CSS to `MemoryGame.module.css`:
.container {
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
.cardGrid {
display: flex;
flex-wrap: wrap;
justify-content: center;
width: 100%;
max-width: 500px;
}
This CSS styles the game container and the card grid.
5. Adding Images (Important!)
You’ll need to add images to your project. Create an `images` folder inside the `public` directory (e.g., `public/images/`). Place six images in this folder (e.g., `image1.jpg`, `image2.jpg`, etc.). Make sure the image paths in the `images` array in `pages/index.js` match the actual file paths of your images. For example:
const images = [
'/images/image1.jpg',
'/images/image2.jpg',
'/images/image3.jpg',
'/images/image4.jpg',
'/images/image5.jpg',
'/images/image6.jpg',
];
If you don’t have images, you can use placeholder images or create simple image files with different colors to test the game functionality.
6. Running and Testing the Game
Save all the files and go back to your browser. If the development server is still running ( `npm run dev`), refresh the page. You should see the memory game! Click on cards to flip them, and try to find matching pairs. The moves counter should update, and you should see a winning message when you match all the pairs.
Common Mistakes and How to Fix Them
- Incorrect Image Paths: The most common issue is incorrect image paths. Double-check that the paths in your `images` array in `pages/index.js` match the actual locations of your image files in the `public/images` directory. Use your browser’s developer tools (usually by right-clicking and selecting “Inspect”) to check for 404 errors (image not found) in the console.
- CSS Conflicts: Make sure your CSS is applied correctly. Check that you’ve imported the CSS module into your components. Use your browser’s developer tools to inspect the elements and see if the CSS styles are being applied. If there are conflicts, try to be more specific with your CSS selectors.
- State Management Errors: Carefully review your state updates (using `setCards`, `setSelectedCards`, etc.). Incorrect state updates can lead to unexpected behavior. Use `console.log` statements to check the values of your state variables at different points in the code to debug state-related issues.
- Incorrect Logic for Matching: Double-check the conditional logic in your `useEffect` hook that checks for matches. Make sure you are comparing the correct properties of the card objects (e.g., `card.image`).
- Missing Dependencies: Ensure you have installed all necessary dependencies. Most of the time, Next.js handles dependencies automatically. If you encounter errors, check the console for any error messages related to missing dependencies.
Key Takeaways
This project has covered the basics of building an interactive memory game using Next.js. You’ve learned how to create components, manage state using `useState`, handle events, conditionally render elements, and use CSS modules for styling. By working through this tutorial, you’ve gained practical experience with key Next.js concepts and web development fundamentals. You can now use these skills as a foundation for building more complex and engaging web applications. Consider adding more features to your game, such as:
- A timer.
- A scoring system.
- A leaderboard.
- More card images.
- Difficulty levels.
- Sound effects.
Experimenting with these enhancements will further solidify your understanding of Next.js and web development principles. The skills you’ve acquired will serve you well as you continue to build more complex and sophisticated web applications.
