In this blog post, we will explore how to implement a memory game using the useReducer hook in React. The memory game is a popular game where players need to find matching pairs of cards within a grid. We will compare the code using useState with the code using useReducer to highlight the advantages of the latter in managing complex state and actions.

Part 1: Understanding the useState Implementation
Let's start by examining the code that uses the useState hook. The component defines the game logic and state using multiple useState hooks. The state variables include the grid, ButtonValue, PrevButtonValue, count, and show. We use these variables to track the game progress, handle button clicks, and determine the number of matches found.

While useState provides a simple way to manage state in smaller applications, it can become cumbersome and harder to maintain as the complexity of the state grows. As we'll see later, the useReducer hook can be a more elegant solution for managing complex state and actions.

Part 2: Introducing the useReducer Implementation
Now, let's dive into the code that uses the useReducer hook. The "MemoryGame" component demonstrates the use of useReducer to manage the game state. It also imports a separate file with game actions and the game reducer.

The useReducer hook allows us to manage state and actions in a more structured way. It takes a reducer function and an initial state as arguments. The reducer function handles different actions and updates the state accordingly. In this implementation, the GameReducer function handles various actions like resetting the game, updating the grid, setting cover and result grids, and managing counts and disables.

By using the useReducer hook, the code becomes more organized, maintainable, and scalable. It separates the concerns of state management and actions, making the code easier to understand and modify.

Part 3: Comparing the useState and useReducer Approaches
Now, let's compare the useState and useReducer implementations side by side. While both implementations achieve the same result, the useReducer approach offers several advantages:

  1. Structured state management: With useReducer, state management is handled by a centralized reducer function, making it easier to understand and modify the state logic.

  2. Better code organization: The useReducer approach separates concerns by keeping the reducer function and actions separate from the component. This improves code organization and maintainability.

  3. Scalability and complex state handling: As the memory game grows in complexity, useReducer handles the state updates more efficiently. It becomes easier to handle actions and update multiple state variables in a consistent manner.

  4. Reusability: The useReducer approach promotes reusability of the reducer function and actions. They can be easily extracted into separate files and reused in different components or even different applications.

Conclusion:
In this blog post, we explored how to implement a memory game using the useReducer hook in React. We compared the code using useState with the code using useReducer and highlighted the advantages of the latter in managing complex state and actions. The useReducer approach provides better code organization, scalability, and reusability, making it a more suitable choice for managing state in larger and more complex applications.

By understanding the benefits of useReducer, developers can leverage this powerful hook to build more maintainable and scalable React applications.

The code for the Memory Game using useReducer consists of two main parts: the MemoryGame component and the game reducer function.

  1. MemoryGame component:

    • The MemoryGame component renders the game grid, buttons, and the current count.
    • It initializes the game state using the useReducer hook and the game reducer function.
    • The state object contains properties such as grid, cover grid, result grid, previous button, show count, count play, and disable.
    • The component renders the game grid and buttons based on the grid property in the state.
    • It handles button clicks by dispatching actions to the reducer function.
    • The component updates the state based on the actions dispatched.
  2. Game reducer function:

    • The game reducer function takes the current state and an action as parameters.
    • It defines action types such as "SET_GRID", "FLIP_CARD", "MATCH_CHECK", "RESET_GAME", and "DISABLE_BUTTONS".
    • Based on the action type, the reducer function updates the state.
    • For example, when the action type is "SET_GRID", it updates the grid, cover grid, and result grid properties in the state.
    • The reducer function uses immutable updates to create a new state object instead of modifying the state directly.

  1. MemoryGame component:

    • useReducer hook: The useReducer hook is used to manage the game state. It takes two arguments: a reducer function and the initial state. It returns the current state and a dispatch function to dispatch actions to update the state.

    • gameReducer function: This is the reducer function that specifies how the state should be updated based on the dispatched actions. It takes the current state and an action as parameters and returns the updated state. The reducer function uses a switch statement to handle different action types.

    • initialState object: This object represents the initial state of the game. It contains properties such as grid, coverGrid, resultGrid, previousButton, showCount, countPlay, and disable. These properties store information about the game grid, the current state of the buttons (whether they are covered or revealed), the previous button clicked, the count of the number of plays, and a flag to disable button clicks during certain game states.

    • dispatch function: This function is returned by the useReducer hook and is used to dispatch actions to update the game state. It takes an action object as an argument, which specifies the action type and optional payload data.

    • setGrid function: This function is responsible for setting up the initial game grid. It takes a size parameter to determine the number of rows and columns in the grid. It generates a random arrangement of button values and dispatches a SET_GRID action with the new grid, cover grid, and result grid.

    • flipCard function: This function is called when a button is clicked. It takes the row and col parameters to identify the clicked button. It dispatches a FLIP_CARD action with the row and column values to reveal the button and check for a match.

    • matchCheck function: This function is called when two buttons are revealed to check if they match. It takes the row and column values of the two buttons and dispatches a MATCH_CHECK action with the row and column values. It also updates the count of plays.

    • resetGame function: This function is called to reset the game state. It dispatches a RESET_GAME action to reset the grid, cover grid, result grid, and other relevant properties to their initial values.

    • disableButtons function: This function is used to disable the button clicks during certain game states. It dispatches a DISABLE_BUTTONS action to update the disable property in the game state.

  2. gameReducer function:

    • The gameReducer function takes the current state and an action as parameters and returns the updated state.
    • Inside the reducer function, a switch statement is used to handle different action types.
    • For each action type, the reducer function creates a new state object using the spread syntax and updates the relevant properties based on the action payload or logic.

These functions work together to manage the game state, handle button clicks, check for matches, reset the game, and update the state accordingly. The useReducer hook simplifies state management by centralizing the state updates in a single reducer function, making the code more organized and maintainable.


UseState

import React, { useEffect, useRef, useState } from "react";

function MemoryGame() {
  const [grid, setGrid] = useState([
    ["๐ŸŒž", "๐ŸŒธ", "๐Ÿ•", "๐ŸŽˆ", "๐Ÿถ"],
    ["๐ŸŽฒ", "๐ŸŒˆ", "๐Ÿฆ", "๐Ÿš€", "๐Ÿฑ"],
    ["๐Ÿ‰", "๐ŸŒบ", "๐ŸŽ", "๐ŸŽ ", "๐Ÿข"],
    ["๐ŸŒž", "๐ŸŒธ", "๐Ÿ•", "๐ŸŽˆ", "๐Ÿถ"],
    ["๐ŸŽฒ", "๐ŸŒˆ", "๐Ÿฆ", "๐Ÿš€", "๐Ÿฑ"],
    ["๐Ÿ‰", "๐ŸŒบ", "๐ŸŽ", "๐ŸŽ ", "๐Ÿข"],
  ]);

  const [ButtonValue, setButtonValue] = useState([]);
  const [PrevButtonValue, setPrevButtonValue] = useState([]);
  const [count, setCount] = useState(0);
  const [show, setShow] = useState(0);

  const handelClick = ({ cell, rowIndex, cellIndex }) => {
    setCount(count + 1);
    setPrevButtonValue(ButtonValue);
    setButtonValue([cell, rowIndex, cellIndex]);
  };

  useEffect(() => {
    if (count > 2) {
      setCount(0);
    }
    if (
      ButtonValue.length !== 0 &&
      PrevButtonValue.length !== 0 &&
      ButtonValue[0] === PrevButtonValue[0]
    ) {
      setShow(show + 1);
      grid[ButtonValue[1]][ButtonValue[2]] = 0;
      grid[PrevButtonValue[1]][PrevButtonValue[2]] = 0;
    }
    setButtonValue("");
    setPrevButtonValue("");
  }, [PrevButtonValue]);

  return (
    <div className="grid grid-flow-row gap-2">
      {grid.map((row, rowIndex) => {
        return (
          <div key={rowIndex} className="grid grid-flow-col px-5 py-3 gap-2 ">
            {row.map((cell, cellIndex) => {
              return (
                <div key={cellIndex}>
                  <button
                    onClick={() => handelClick({ cell, rowIndex, cellIndex })}
                  >
                    {cell}
                  </button>
                </div>
              );
            })}
          </div>
        );
      })}
      <div>{count}</div>
      <div>
        {ButtonValue}-{PrevButtonValue}
      </div>
      <div>{show ? `Win one ${show} cell` : ""}</div>
    </div>
  );
}

export default MemoryGame;

useReducer

"use client";

import React, { useEffect, useRef, useReducer } from "react";
import GAME_ACTIONS from "./reducer/GameActions";
import { GameReducer, INITIAL_STATE } from "./reducer/GameReducer";

function MemoryGame() {
  const [state, dispatch] = useReducer(GameReducer, INITIAL_STATE);

  const timeoutRef = useRef(null);
  const isGameWon = state.showCount === 15;
  const isGameLost = state.CountPlay > 45;

  const handleClick = (cell, rowIndex, cellIndex) => {
    const newGrid = (() => {
      const MyNewGrid = [...state.CoverGrid];
      MyNewGrid[rowIndex][cellIndex] = true;
      return MyNewGrid;
    })();

    dispatch({
      type: GAME_ACTIONS.SET_COVER_GRID,
      payload: newGrid,
    });

    if (state.prevButton) {
      if (
        rowIndex === state.prevButton.row &&
        cellIndex === state.prevButton.col
      ) {
        return;
      }
      dispatch({
        type: GAME_ACTIONS.SET_COUNT,
        payload: state.count + 1,
      });
      if (cell === state.prevButton.value) {
        const newGrid = [...state.ResultGrid];
        newGrid[rowIndex][cellIndex] = true;
        newGrid[state.prevButton.row][state.prevButton.col] = true;
        dispatch({
          type: GAME_ACTIONS.SET_RESULT_GRID,
          payload: newGrid,
        });
        dispatch({
          type: GAME_ACTIONS.SET_SHOW_COUNT,
          payload: state.showCount + 1,
        });
      }
      dispatch({
        type: GAME_ACTIONS.SET_PREV_BUTTON,
        payload: null,
      });
    } else {
      dispatch({
        type: GAME_ACTIONS.SET_PREV_BUTTON,
        payload: { row: rowIndex, col: cellIndex, value: cell },
      });
      dispatch({
        type: GAME_ACTIONS.SET_COUNT,
        payload: state.count + 1,
      });
    }
  };

  const handleMatch = () => {
    dispatch({
      type: GAME_ACTIONS.SET_DISABLE,
      payload: false,
    });
    dispatch({
      type: GAME_ACTIONS.SET_PREV_BUTTON,
      payload: null,
    });
    dispatch({
      type: GAME_ACTIONS.SET_COUNT,
      payload: 0,
    });
    const newGrid = state.ResultGrid.map((row) => [...row]);
    dispatch({
      type: GAME_ACTIONS.SET_COVER_GRID,
      payload: newGrid,
    });
    dispatch({
      type: GAME_ACTIONS.SET_COUNT_PLAY,
      payload: state.CountPlay + 1,
    });
  };

  useEffect(() => {
    if (timeoutRef.current) {
      clearTimeout(timeoutRef.current);
    }
    if (state.count === 2) {
      dispatch({
        type: GAME_ACTIONS.SET_DISABLE,
        payload: true,
      });
      timeoutRef.current = setTimeout(handleMatch, 500);
      return () => clearTimeout(timeoutRef.current);
    }
    if (isGameLost) {
      dispatch({
        type: GAME_ACTIONS.SET_DISABLE,
        payload: true,
      });
    }
  }, [state.prevButton, state.count, state.CountPlay, isGameLost]);

  useEffect(() => {
    dispatch({ type: GAME_ACTIONS.RESET_GAME });
  }, []);

  return (
    <div className="grid grid-cols-1 max-w-screen-2xl mx-auto mt-24 h-full">
      <div className="flex flex-col items-center layout-root-variant my-4 border-gray-600 rounded-md border-2 p-4 mx-3 sm:mx-0">
        The game utilizes the power of the useReducer hook to manage the game
        state, handle card flipping, track matches, and determine when all pairs
        have been found. By diving into the code shared on our blog, you can
        gain valuable insights into how the game mechanics are implemented using
        useReducer.
      </div>

      <div className="flex flex-col items-center layout-root-variant my-4 border-gray-600 rounded-md border-2 p-4 mx-3 sm:mx-0">
        <div className="grid grid-flow-row gap-1  w-full sm:w-2/4">
          {state.grid.map((row, rowIndex) => {
            return (
              <div
                key={rowIndex}
                className="grid grid-flow-col px-3 py-2 gap-1 text-center"
              >
                {row.map((cell, cellIndex) => {
                  return (
                    <div key={cellIndex}>
                      {state.CoverGrid[rowIndex][cellIndex] ? (
                        state.ResultGrid[rowIndex][cellIndex] ? (
                          <button
                            disabled
                            onClick={() =>
                              handleClick(cell, rowIndex, cellIndex)
                            }
                          >
                            {cell}
                          </button>
                        ) : (
                          <button
                            disabled={state.Disable}
                            onClick={() =>
                              handleClick(cell, rowIndex, cellIndex)
                            }
                          >
                            {cell}
                          </button>
                        )
                      ) : (
                        <button
                          disabled={state.Disable}
                          onClick={() => handleClick(cell, rowIndex, cellIndex)}
                          className="border border-red-600 rounded-md p-3 sm:!p-5"
                        >
                          โŒ
                        </button>
                      )}
                    </div>
                  );
                })}
              </div>
            );
          })}
        </div>
      </div>
      <div className="flex flex-col items-center layout-root-variant my-4 border-gray-600 rounded-md border-2 p-4 mx-3 sm:mx-0">
        <div className="flex justify-center gap-8">
          <div className="text-2xl font-bold text-blue-400">
            {state.showCount ? `Matched cells: ${state.showCount}` : ""}
          </div>
          <div
            className={`${
              state.CountPlay > 30 ? "text-red-500" : "text-green-500"
            } text-2xl font-bold`}
          >
            {state.CountPlay ? `Play count: ${state.CountPlay}` : ""}
          </div>
        </div>

        <div>
          {isGameWon && (
            <div className="flex justify-center">
              <div className="text-4xl font-bold text-green-500">
                Congratulations! You won the game.
              </div>
            </div>
          )}

          {isGameLost && (
            <div className="flex justify-center">
              <div className="text-xl font-bold text-red-500">
                Sorry! You lost the game.
                <button
                  className="ml-4 text-xl font-bold text-blue-500"
                  onClick={() => {
                    dispatch({
                      type: GAME_ACTIONS.SET_DISABLE,
                      payload: true,
                    });
                    dispatch({ type: GAME_ACTIONS.RESET_GAME });
                  }}
                >
                  Play Again
                </button>
              </div>
            </div>
          )}
        </div>
      </div>
    </div>
  );
}

export default MemoryGame;
const GAME_ACTIONS = {
  RESET_GAME: "RESET_GAME",
  SET_GRID: "SET_GRID",
  SET_COVER_GRID: "SET_COVER_GRID",
  SET_RESULT_GRID: "SET_RESULT_GRID",
  SET_PREV_BUTTON: "SET_PREV_BUTTON",
  SET_SHOW_COUNT: "SET_SHOW_COUNT",
  SET_COUNT: "SET_COUNT",
  SET_COUNT_PLAY: "SET_COUNT_PLAY",
  SET_DISABLE: "SET_DISABLE",
  RESET_STATE: "RESET_STATE",
};

export default GAME_ACTIONS;
import GAME_ACTIONS from "./GameActions";
import { cloneDeep } from "lodash";

const INITIAL_GRID = [
  ["๐ŸŒž", "๐ŸŒธ", "๐Ÿ•", "๐ŸŽˆ", "๐Ÿถ"],
  ["๐Ÿฆ", "๐Ÿš€", "๐Ÿฑ", "๐ŸŽฒ", "๐ŸŒˆ"],
  ["๐Ÿ‰", "๐ŸŒบ", "๐ŸŽ", "๐ŸŽ ", "๐Ÿข"],
  ["๐ŸŽˆ", "๐Ÿถ", "๐ŸŒž", "๐ŸŒธ", "๐Ÿ•"],
  ["๐ŸŽฒ", "๐ŸŒˆ", "๐Ÿฆ", "๐Ÿš€", "๐Ÿฑ"],
  ["๐ŸŽ", "๐ŸŽ ", "๐Ÿข", "๐Ÿ‰", "๐ŸŒบ"],
];

const RandomGrid = (initialGrid) => {
  const newGrid = initialGrid.map((row) => [...row]);
  for (let i = newGrid.length - 1; i > 0; i--) {
    for (let j = newGrid[i].length - 1; j > 0; j--) {
      const k = Math.floor(Math.random() * (i + 1));
      const l = Math.floor(Math.random() * (j + 1));
      [newGrid[i][j], newGrid[k][l]] = [newGrid[k][l], newGrid[i][j]];
    }
  }
  return newGrid;
};

export const INITIAL_STATE = {
  grid: RandomGrid(INITIAL_GRID),
  CoverGrid: Array.from({ length: INITIAL_GRID.length }, () =>
    Array(INITIAL_GRID[0].length).fill(false)
  ),
  ResultGrid: Array.from({ length: INITIAL_GRID.length }, () =>
    Array(INITIAL_GRID[0].length).fill(false)
  ),
  prevButton: null,
  showCount: 0,
  count: 0,
  CountPlay: 0,
  Disable: false,
};

export const GameReducer = (state = INITIAL_STATE, action) => {
  switch (action.type) {
    case GAME_ACTIONS.RESET_GAME:
      return cloneDeep(INITIAL_STATE);
    case GAME_ACTIONS.SET_GRID:
      return {
        ...state,
        grid: action.payload,
      };
    case GAME_ACTIONS.SET_COVER_GRID:
      return {
        ...state,
        CoverGrid: action.payload,
      };
    case GAME_ACTIONS.SET_RESULT_GRID:
      return {
        ...state,
        ResultGrid: action.payload,
      };
    case GAME_ACTIONS.SET_PREV_BUTTON:
      return {
        ...state,
        prevButton: action.payload,
      };
    case GAME_ACTIONS.SET_SHOW_COUNT:
      return {
        ...state,
        showCount: action.payload,
      };
    case GAME_ACTIONS.SET_COUNT:
      return {
        ...state,
        count: action.payload,
      };
    case GAME_ACTIONS.SET_COUNT_PLAY:
      return {
        ...state,
        CountPlay: action.payload,
      };
    case GAME_ACTIONS.SET_DISABLE:
      return {
        ...state,
        Disable: action.payload,
      };
    case GAME_ACTIONS.RESET_STATE:
      return INITIAL_STATE;

    default:
      return state;
  }
};

Masoud

March 1st, 2026