Building a Hangman Game with React & Typescript

3 min read

I recently developed a Hangman game with React and TypeScript as a quick example of how to use modern web technologies to create a fun and engaging user experience.

Before we dive into the code, let's first take a look at the technologies used in building this project:

  • React

    A popular JavaScript library for building user interfaces

  • TypeScript

    A strict syntactical superset of JavaScript that adds optional static typing

  • Immer

    A library for working with immutable state in a more convenient way

  • Jest

    A delightful JavaScript testing framework

  • React Testing Library

    A simple and complete testing library for React components

  • Emotion CSS

    A library for writing CSS in JS

React and TypeScript

To get started I bootsrapped the project with Create React App. I used TypeScript template to enforce strict typing and ensuring that the code is more robust and less prone to errors.

npx create-react-app hangman --template typescript

useReducer and Immer

React.useReducer and immer are powerful tools when combined, providing a clean and efficient solution for managing game states, like in the Hangman application.

useReducer is a built-in React hook that allows you to manage state changes within a component using a reducer function. This function receives the current state and an action object, and it returns a new updated state based on the action type. The useReducer hook promotes predictability and testability by centralizing state logic in one place, which is particularly helpful for applications like Hangman.

Immer is a library that simplifies handling immutable state updates by allowing you to write code as if you were directly mutating the state. It achieves this by leveraging JavaScript's Proxy objects to create a "draft" of the original state, which can be safely mutated. Once all modifications are complete, Immer produces a new, immutable state object based on the draft.

To integrate immer with useReducer in the Hangman game, we first import the library and wrap our reducer function using the produce function from immer:

import produce, { Draft } from "immer";
 
const gameReducer = (draft: Draft<GameState>, action: GameAction) => {
  switch (action.type) {
    case "start": {
      draft.status = GameStatus.Playing;
      return;
    }
    // ...etc
    default: {
      return;
    }
  }
};
 
export const curriedGameReducer = produce(gameReducer);

Testing

I used React Testing Library and Jest to test each layer of the application from the components, to the state and the views. These are my preffered go to tools as they are simple to use, provide a great developer experience and encourage testing user interactions.

import { fireEvent, render } from "@testing-library/react";
import { ThemeProvider } from "@emotion/react";
 
import App from ".";
import GameContext, {
  GameDispatchContext,
  GameStateContext,
} from "../../context/Game";
import { initialState, GameStatus } from "../../context/Game/state";
import { THEMES } from "../../config";
 
describe("<App />", () => {
  it("renders the game when status is NOT 'notStarted' ", () => {
    const playingState = {
      ...initialState,
      chancesRemaining: 6,
      guesses: [],
      solution: "solution",
      solutionFormatted: "_______",
      status: GameStatus.Playing,
      topic: "words" as const,
    };
 
    const { getByText } = render(
      <GameDispatchContext.Provider value={jest.fn()}>
        <GameStateContext.Provider value={playingState}>
          <ThemeProvider theme={THEMES.light}>
            <App />
          </ThemeProvider>
        </GameStateContext.Provider>
      </GameDispatchContext.Provider>
    );
 
    expect(getByText(`(chances remaining: 6)`)).toBeInTheDocument();
  });
});

Styling with Emotion

The app's styles are created using the Emotion library, which allows you to write CSS in JavaScript. This makes it easy to create dynamic, responsive styles that are tightly coupled with the components they're applied to.

/** @jsxImportSource @emotion/react */
import React from "react";
import { css, useTheme } from "@emotion/react";
 
export interface LetterProps {
  onClick: (letter: string) => void;
  isCorrect: boolean;
  isDisabled: boolean;
  letter: string;
  shouldHighlight: boolean;
}
 
const Letter = React.memo<LetterProps>(
  ({ onClick, isCorrect, isDisabled, letter, shouldHighlight }) => {
    const { border, breakpoints, colors, fontSize } = useTheme();
 
    return (
      <button
        disabled={isDisabled}
        onClick={() => onClick(letter)}
        css={css`
          background-color: transparent;
          border: ${border.width}px dashed transparent;
          border-radius: 50%;
          color: ${shouldHighlight
            ? isCorrect
              ? colors.success
              : colors.error
            : colors.text};
          display: inline-block;
          font-size: ${fontSize[2]}px;
          height: 36px;
          margin: 4px;
          outline: none;
          padding: 2px 10px 10px 10px;
          text-align: center;
          transition: border 200ms ease-out;
          width: 36px;
          &:hover:enabled {
            border: ${border.width}px dashed ${colors.grey};
            cursor: pointer;
          }
          @media (min-width: ${breakpoints.md}) {
            font-size: ${fontSize[3]}px;
            height: 50px;
            width: 50px;
          }
        `}
      >
        {letter}
      </button>
    );
  }
);
 
export default Letter;

Overall I had a lot of fun making this quick game, and so did my kids! It was cool to show them the power of React and how quickly we could put together a little game, although deciding on the best topics was impossible!

You can find the source code on GitHub for this project.