Edd Mann Developer

Building a Flag Guessing Game using React Hooks

Since React Hooks were announced last autumn I’ve been looking at ways to experiment with them in a couple of small side-projects. In this post we will create a simple flag guessing game (called Fun with Flags) leveraging Create React App, Styled Components and React Hooks. Along the way we will highlight use-cases for several different hooks such as State, Effect and Memoization. Finally, we will automate the process of publishing the compiled game to GitHub Pages.

Sheldon Cooper Presents: Fun with Flags

Setup

The first step we need to take is to initialise the project, including the required styled-components dependency.

$ yarn create react-app fun-with-flags
$ cd fun-with-flags && yarn add styled-components

The game will take advantage of the flag emojis, which we store in src/flags.js along with their associated name. If you are following along with this post, you can find the completed listing within the GitHub repository.

export default [
  {
    emoji: "🇬🇧",
    name: "United Kingdom"
  },
  // ...
];

The Answer Box

It is good practise to decompose an application into smaller, high cohesion units - of which we can then compose together to provide the desired behaviour. With this in mind, we will begin by focusing our attention on the special input field (located within src/AnswerBox.js) that will be used to manage the user’s entered answer.

We start off by defining some key styled building blocks used to create the overall AnswerBox.

import React, { useState, useEffect, useMemo } from "react";
import styled from "styled-components";

const Form = styled.form`
  margin-bottom: 1em;
`;

const Placeholder = styled.span`
  font-family: courier;
  padding: 0 0 0.2em 0.2em;
  font-size: 2em;
  letter-spacing: 0.2em;
`;

const HiddenInput = styled.input`
  opacity: 0;
  position: absolute;
  width: 100%;
  top: 0;
  right: 0;
  bottom: 0;
  left: 0;
  caret-color: transparent;
  &:focus + ${Placeholder} {
    box-shadow: 0 0 5px rgba(81, 203, 238, 1);
  }
`;

The input will be presented to the user in placeholder form, using a _ to indicate the presence of a character and including any word separators. To achieve this we will use a hidden text input within a form, displaying the resulting input value within a styled span element presented to the user. In doing this we can still take advantage of the form lifecycle events (i.e. onChange and onSubmit).

Thank you to Sebastian Sim for this idea!

const toPlaceholder = (value, answer) =>
  [...value].reduce((placeholder, char) => {
    return placeholder.replace("_", char);
  }, answer.replace(/[^\s]/g, "_"));

const normalise = value => value.toUpperCase().replace(/[^A-Z]/g, "");

export default ({ answer, onCorrect, onIncorrect, ...props }) => {
  const [value, setValue] = useState("");

  useEffect(() => setValue(""), [answer]);

  const handleChange = event => {
    setValue(normalise(event.target.value));
  };

  const handleSubmit = event => {
    event.preventDefault();

    value === normalise(answer) ? onCorrect() : onIncorrect();
    setValue("");
    return;
  };

  const placeholder = useMemo(() => toPlaceholder(value, answer), [
    value,
    answer
  ]);

  const maxLength = useMemo(() => normalise(answer).length, [answer]);

  return (
    <Form onSubmit={handleSubmit}>
      <HiddenInput
        onChange={handleChange}
        value={value}
        maxLength={maxLength}
        autoFocus
      />
      <Placeholder>{placeholder}</Placeholder>
    </Form>
  );
};

Looking at the implementation above you can see that we manage the user’s answer with a State hook. This value is reset if the desired answer changes (i.e. a new flag is displayed) using an Effect hook. From here, both calculating the actual answer’s length and generating the placeholder for the current state are referentially transparent operations, and as such can be cached using the Memoization hook. Finally, when the user submits the form (i.e. hits enter), we check the value and based on its correctness invoke the provided onCorrect/onIncorrect callbacks.

Managing the Game

With the AnswerBox now implemented we can move on to developing the encompassing Game management component within src/Game.js. In a similar manner to before, we define several styled building blocks that will be used to present the overall game to the user.

import React, { useState, useEffect } from "react";
import styled from "styled-components";
import AnswerBox from "./AnswerBox";

const CentreWrapper = styled.div`
  margin: 0;
  padding: 0;
  overflow: hidden;
  position: absolute;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  font-family: sans-serif;
`;

const Flag = styled(({ flag, ...props }) => <div {...props}>{flag}</div>)`
  font-size: 5em;
  line-height: 1em;
  padding: 0;
  margin: 0;
`;

const Results = styled(({ score, attempts, ...props }) => (
  <div {...props}>
    Score: <span>{score}</span>, Attempts: <span>{attempts}</span>
  </div>
))`
  display: block;
  font-size: 1.5em;
  span {
    font-weight: bold;
  }
`;

This component manages the current score, attempts and flag ordering - delegating to the AnswerBox for presenting and verifying the answer input.

const shuffle = arr => [...arr].sort(() => 0.5 - Math.random());

export default props => {
  const [flags, setFlags] = useState(() => shuffle(props.flags));
  const [score, setScore] = useState(0);
  const [attempts, setAttempts] = useState(props.attempts);

  const nextFlag = () => {
    setFlags(flags.length > 1 ? flags.slice(1) : shuffle(props.flags));
    setAttempts(props.attempts);
  };

  const onCorrect = () => {
    setScore(score + 1);
    nextFlag();
  };

  const onIncorrect = () => {
    if (attempts > 1) {
      setAttempts(attempts - 1);
      return;
    }

    setScore(Math.max(0, score - 1));
    nextFlag();
  };

  const [{ emoji, name }] = flags;

  return (
    <CentreWrapper>
      <Flag flag={emoji} />
      <AnswerBox
        answer={name}
        onCorrect={onCorrect}
        onIncorrect={onIncorrect}
      />
      <Results score={score} attempts={attempts} />
    </CentreWrapper>
  );
};

When the game is started (or all flags exhausted) we reset the flags state to a shuffled ordering of the provided listing. In the case of the flags initial state, we only wish to shuffle the provided flags once at the beginning (not wastefully on each render). To do this we wrap the behaviour in a function which ensures that the operation is only called when required during the components lifecycle. From here, based on the decision made within the AnswerBox component, we update the user’s attempts/score accordingly.

Finally, we are able to compose all these pieces together into a src/index.js entry point.

import React from "react";
import ReactDOM from "react-dom";
import Game from "./Game";
import flags from "./flags";

ReactDOM.render(
  <Game flags={flags} attempts={3} />,
  document.getElementById("root")
);

We can then yarn start to experiment with the game locally 🎉.

Publishing to GitHub Pages

Now that we are happy with the game locally, the next step is to share it with the masses 😎. This can be easily achieved via compiling a production build locally and publishing it to GitHub Pages. We can even automate this process by adding the following gh-pages dependency.

$ yarn add gh-pages

With this dependency added, include the following scripts actions into the package.json file.

{
  "scripts": {
    "predeploy": "yarn build",
    "deploy": "gh-pages -d build"
  }
}

Now, whenever you wish to publish a new version of the game simply run the following command and gh-pages will build and commit a new version to the gh-pages branch.

$ yarn deploy

This in-turn will publish a new static version of the game to GitHub Pages, accessible in my case via https://eddmann.github.io/fun-with-flags and as I have my apex DNS record pointing to GitHub at https://eddmann.com/fun-with-flags.

Fun with Flags