2 min read

Tutorial: How to build a memory game

💡
This is a tutorial that solves the "Build a memory game" exercise.
If you haven't already, try to solve it yourself first before reading the solution!

We want to build a component that takes in an array of 6 images and outputs the memory game displaying 12 cards.

Generate the card images

The first step is to generate the card images - given 6 initial images, we will need to double them, so that we have 2 of each, and then shuffle the array:

// given 6 images, generate 12 cards for the game
  const gameImages = shuffle([...images, ...images]);
  return (
    <div>
      {gameImages.map((image) => (
        <img src={image} />
      ))}
    </div>
  );

To shuffle the images, we're using the lodash library, which has a handy shuffle method (https://www.geeksforgeeks.org/lodash-_-shuffle-method/)

Let's check that this works by refreshing the page and seeing that we get a different order every time.

0:00
/

Start by showing placeholders

Next, let's hide the images, and just show placeholders instead. When the user clicks an image, we can flip it.

We can keep an index of the currently flipped image in the component state:

  const [flippedIndex, setFlippedIndex] = useState(null);
  // given 6 images, generate 12 cards for the game
  const gameImages = shuffle([...images, ...images]);
  return (
    <div>
      {gameImages.map((image, index) =>
        index === flippedIndex ? (
          <img src={image} />
        ) : (
          <div
            className="imagePlaceholder"
            onClick={() => setFlippedIndex(index)}
          ></div>
        )
      )}
    </div>
  );
0:00
/

This works, but notice that the images get re-shuffled after every click! This is not what we want.
So we need to extract the shuffling outside of this component, to prevent rerendering it.

Let's create an ImageBoard component, just for the images grid.

const MemoryGame = ({ images }) => {
  // given 6 images, generate 12 cards for the game
  const gameImages = shuffle([...images, ...images]);
  return <ImageBoard images={gameImages} />;
};

Keep images visible when a match is found

As the user clicks through the images, if he gets a different one every time, we flip the image back again. But if he clicks the same image twice, it means he has a match, and we can keep the images visible!

We'll need to remember for each image if it was "found" or not, so we can keep it visible.

This would be the final component:

const ImageBoard = ({ images }) => {
  const [flippedIndex, setFlippedIndex] = useState(null);
  const [foundImages, setFoundImages] = useState({});

  const handleImageClick = (index) => {
    if (images[flippedIndex] === images[index]) {
      // if current and previous images match,
      // make them visible
      setFoundImages({ ...foundImages, [index]: true, [flippedIndex]: true });
    }
    setFlippedIndex(index);
  };

  const isImageVisible = (index) => {
    if (foundImages[index] === true) {
      // if image was "found"
      return true;
    }
    if (index === flippedIndex) {
      // if image was just flipped
      return true;
    }
    return false;
  };

  return (
    <div>
      {images.map((image, index) =>
        isImageVisible(index) ? (
          <img src={image} />
        ) : (
          <div
            className="imagePlaceholder"
            onClick={() => handleImageClick(index)}
          ></div>
        )
      )}
    </div>
  );
};

You can also check out a working app in this CodeSandbox: https://codesandbox.io/p/sandbox/memory-game-xdqzk3

0:00
/