Overview
Wordle is a word puzzle game that blew up in popularity during early 2022, in no small part due to the COVID pandemic.
In Wordle, the player has 6 attempts to guess a five-letter word, with the colors of the letters telling the player how accurate their guesses were. Gray means the letter's not in the word, yellow means the letter is in the word but in the wrong position, and green means the letter is in the right position.
In this article we'll create a website that's a clone of Wordle, using the following:
- TypeScript — TypeScript is a programming language that adds type checking to javascript code. Most javascript developers use typescript to ensure that their code is type-safe and correct.
- React — React is the world's most popular framework for building websites.
- Redux & Redux Toolkit — Redux is a state management library that helps handle complex application logic, it is often paired with React. Redux Toolkit is a library that helps reduce much of Redux's boilerplate, and is the recommended way to write Redux code.
- CSS Modules — CSS Modules adds local scoping to css class names, this helps prevent name collisions when naming css classes for multiple components. This feature is integrated as part of vite.
- Vite — Vite is a popular javascript build tool and code bundler.
Prerequisites
This tutorial assumes you have some basic experience using React, if not I recommending taking a beginners React tutorial before starting this one.
Project Download
The source code for the completed project can be downloaded here: nerdle.zip.
Coding
With that out of the way, let's open our text editor and start coding!
Creating a Vite project
First make sure you have node.js and npm installed.
To avoid trademark issues with New York Times, I'm going to call our project nerdle
.
If you'd like you can replace it with another name, just don't come to me if the
NYT lawyers come knocking at your door.
We will use vite to create our project. In your terminal, navigate to a folder where you'd like your project directory to be created, then run:
Select the framework React
with variant TypeScript
.
Voilà, you now have an entire React app scaffolded out! Run these commands to install all required dependencies and start the development server:
Vite should display a local URL that you can use to view your website locally in a browser.
Entering the URL into your browser, you should see the default project webpage for React.
Open the nerdle
directory in your favorite code editor. You should see a
project directory similar to the following:
Planning the user interface
Before starting to code, it's helpful to create a mock up of what your user interface will look like. This doesn't have to be fancy, often a quick sketch on a piece of paper is enough. What's important is that you're able to identify the major components of your design and how they are positioned relative to each other.
Here's the mock up we'll use for our game:
We've got 4 major components here, lined up vertically in the middle of the page:
- Title — This will just display our game's name: Nerdle.
- Game Board — A 5-by-6 grid of colored tiles with letters. Each tile is one of 4 colors: yellow, green, black (dark gray), or empty (no color).
- Results Text — Displays a message when the game is won or lost, otherwise will be empty.
- Keyboard — A grid of keyboard buttons, with some keys colored depending on the game state.
Constructing the skeleton layout
Now that we have a basic design in mind, let's start building our app!
A great place to start whenever you're coding a web application is to code the ui components of your app without any logic or functionality.
Delete everything in the src/
directory except for main.tsx
, index.css
, App.tsx
, and vite-env.d.ts
.
Our main.tsx
should look something like this:
In this file, we take the DOM node with id root
(see index.html
)
and set that as our React root, then render the main app component into that div.
React.StrictMode
will cause react to do some extra checks
during development to ensure that our code is correct.
Delete everything in src/index.css
and replace it with the following:
For this project we will make use of CSS modules so that our styles are locally
scoped to each component, thus our global index.css
file won't contain much styling.
Let's create our main app component. Replace the contents of src/App.tsx
with:
Create the file src/App.module.css
, then add the following styles:
The main app div is styled as a single column using display: flex
and some other properties,
just as we wanted in our design.
Notice the .module.css
extension, this enables the CSS modules feature which
allows us to import the css class names as strings from within javascript!
Let's finish up the app skeleton with the board and keyboard components.
The board component is straightforward, it's simply a 5×6 grid of cells. We can make use of CSS grid to achieve this. Create the following files:
Here we generate a list of empty cells and set that as the children of our board element.
Notice how we set a unique key
string for each cell in the list:
In React, it is important that all elements in a list have a unique key
property
so that during rendering React can match elements in a list
to maintain performance.
Next, let's create our keyboard component.
In this component we will have nodes with multiple class names,
let's install the popular classnames
npm utility module to help
manage multiple class names on a DOM node:
Next let's create the keyboard component.
We define the small component <Key />
that represents a key on the on-screen keyboard,
it will take some properties like char
and wide
to control how it is displayed.
Then we construct our main keyboard component from a bunch of <Key />
components.
Our keyboard is styled in 3 rows, with each key being 40×48 px in dimension. The enter and backspace keys are slightly wider at 64×48 px.
The classnames utility combines class names together, removing any falsy values.
For example, cn("key", "wide") === "key wide"
, and
cn("key", false) === "key"
.
Notice that the wide
property of the <Key />
component has no value,
in React this is short hand for the boolean true.
That is, <Key wide />
is equivalent to <Key wide={true} />
.
Let's add these components to our App component:
Our app skeleton layout is now complete. If you followed all the steps properly, your app should now look something like this:
Creating the Redux store
In react, the basic way of managing application state is using the useState hook. This works fine for smaller apps but when your application becomes larger it's useful to use a library to manage state across your whole app.
This tutorial uses redux for state management since it's one of the most popular state management libraries used with react.
Create a new directory src/store
, this is where we'll put our state management code.
Next, download the wordlist.json file and move it to src/store/wordlist.json
.
This file contains the list of target words, as well as all the words that are valid
guesses for our game.
Now, we'll start constructing our redux store. First we'll need to install redux, as well as the bindings for redux with react.
In redux, application state is stored in a global object called the 'store'. Unlike most state management code, redux does not allow you to directly mutate data in the store. Instead, you must dispatch an object called an 'action' to the store, which contains info about how you'd like to update the store. The action is passed to a reducer function which takes an action and the current state of the store and accordingly creates a new version of the store state.
This approach to state management is called the 'flux' design pattern, and leads to a uni-directional data flow which is easy to follow and debug. For example, you can install the redux devtools browser extension which allows you to "time travel" across snapshots of your application state to quickly identify bugs with your redux store.
Redux toolkit provides convenient helpers for defining our redux store; for
example, the createSlice
function allows us to define the initial application
state, actions that modify that state, and reducer functions that take actions
to update the state, all using one function. Typically a complex
application will define multiple slices to separate different parts of their
app's data, however since our app is pretty simple we'll only need one slice.
Let's first define the shape and initial state of our store. Create
src/store/index.ts
, then write:
Our game state has 4 properties:
target
— The target word being guessed by the player.guesses
— A list of the guesses that the player has made.input
— The letters that our player currently is typing, always between 0-5 chars long.gameOver
— Whether the game is over, true when the player has guessed the word or used all 6 guesses.
Next, we'll create our redux slice. The first action we will implement is the start action, which randomly chooses the target word and initializes the game state.
Note that each redux reducer function must be a pure function—given the same action and previous state the reducer function must always return the same value. This is an important rule to follow to make your state management code predictable and debuggable.
In this case, a random "seed" number is passed to the start function to make the chosen target word deterministic. Later in the react component we will pass a random seed to this function.
Next, let's create actions for the user inputting a letter, pressing backspace, and pressing enter to submit a guess.
Note that with redux toolkit, your reducer functions can either return a new value for the state or directly mutate the state value.
Finally we'll create the redux store and export all our store actions, as well as a selector function which will be used by our react components to access our store.
All together, your file should look like this:
Crafting some algorithms
Before we connect our redux store to the react components, we need to take a brief detour and write some helper functions.
The first function we'll write is an algorithm to determine the colors of a guessed word given a target word. For the colors we'll use the convention "G" = green, "Y" = yellow, and "B" = black, our function will return a string with one of "G", "Y" or "B" for each letter in the guess. A letter should be green if it's in the correct position in the target word, yellow if it's in the target word but in a wrong position, and black if it isn't in the target word.
One edge case that's often overlooked with coloring wordle guesses is how duplicate unmatched letters are handled. For example, what should the coloring for MAMMA be if the target word is MAXIM?
The correct coloring is GGYBB: notice that the second M is yellow, while the third M is black. The rule for duplicate letters is that, after matching green letters in the guess word, each remaining unmatched letter in the target word is assigned to one yellow letter in the guess word in left-to-right order. Thus for a given letter, the number of green and yellow letters in the guess word should never exceed the number of that letter in the target word.
Create the file src/funcs.ts
, this is where we'll place our helper functions.
The following is an implementation of the wordle coloring algorithm:
The second helper function we need to write is a function that returns the color of the keys on the keyboard. A letter on the keyboard is green if it's been guessed in the right position, yellow if it's been guessed in an incorrect position, black if it's not in the word, and gray if it's not been guessed in a word yet.
The idea for the algorithm is that for a given letter, we look through all the colored letters in every guess and keep track of the "best" color for that given letter. For example, if our letter is 'P', if we find a yellow P in one word and also a green P in another word, the best color for P is green. If P isn't found in any words, then we return an empty string.
Below is our algorithm for getting the key color:
Wiring it all together
Alright, we're almost done! Now that we have all our state management code, we just have to hook it up to our react components.
First in src/main.tsx
, we need to wrap our app component in the react-redux
<Provider/>
component, so that our react components have access to our
redux store:
First let's wire up our state to the board component.
We will use the useAppSelector
hook to access parts of our store. This hook
will rerender our component whenever the part of the store that it accesses
changes.
Add the following code to src/Board.tsx
so that it displays the state of our game:
We'll also need to add new classes for each of the 3 cell colors. Note
that in CSS, a selector of the form .classA.classB
will select DOM nodes
that have the class classA
and classB
:
Next we'll hook up our keyboard to work with the store.
To listen to keyboard events, we will use the useEffect hook to add a keyboard listener to the global window object after the keyboard component renders for the first time.
We'll use the useDispatch
hook, which returns a dispatcher function that we can use to
dispatch actions to our store.
Make the following changes to src/Keyboard.tsx
:
Lastly, we'll update src/App.tsx
to start the game once the component loads,
and also update the status text depending on whether the user has won / lost
the game.
If everything went correctly, you should now have a functioning game of Wordle running in your browser!
You should be able to type guesses with both the on-screen and your physical keyboard, an appropriate warning should show if you input something that isn't a word. A message should show whenever you win or lose the game. You can restart the game with a new random word by refreshing the page.
Further Ideas
Congratulations on completing the tutorial! If you're hungry for a bigger challenge, here are some further ideas for this project:
- Wordle is typically a daily game with a unique word to guess each day. Can you make the word unique for each day, instead of randomly generating it on page load?
- Can you implement a stats system, so that the user can see their scores for previous games? This may involve storing data to the browser's local storage.
- Can you add a timer that tracks how long a player took to complete the game? Hint: store the start time in the store when the user types their first guess.
- Currently
wordColor
is run 27 times per render (once for the board, and 26 times for each letter in the keyboard). Can you improve the efficiency of the program by reducing the number of calls to this function during each rerender?