How to Build “Wordle” Using ReactJS and About 200 Lines of Sloppy Code

crashdaddy
16 min readFeb 21, 2022
It’s easier than you may think

For the last couple of weeks, my news feed has been cluttered full of headlines gushing about this “new” game called Wordle. I just checked, and it’s still making news headlines as recently as six minutes ago. Go figure.

Programmers will recognize it immediately as a clone of the game Mastermind, after however many years of recreating that game as an exercise in different languages.

So let’s do it again! This time, instead of colored blocks or numbers, we’ll be building ̶M̶a̶s̶t̶e̶r̶m̶i̶n̶d̶ , I mean Wordle using words.

First, scamper over to https://wordlier.netlify.app/ and take a look at a working demo of this game, and then let’s get started!

Step 1: Imagine the App

What do we want our game to do?

Let’s start by choosing the length of the word the player has to figure out. Most people go with 5-letter words. I’ve seen some versions where the user can adjust the word length in order to increase or decrease difficulty.

Then we’ll want to give the player, say, six turns to figure out the word. How they do that is by picking a word to start with. The app will then tell them how many letters in the word they picked correspond to letters in the actual word they’re trying to figure out.

Here’s an example:

I even lose at game I wrote myself

As you can see in the above image, the first word the player chose was “arise” (lots of vowels — smart). It had a letter “A” in it, which does occur in the app’s chosen word, but not at the same place in the word. That’s why it’s colored blue.

If it was the right letter and in the right place it would be colored green.

When all the letters are green, the player wins. If the player takes six turns and can’t figure out the word, the player loses.

Get it? It’s pretty fun, ngl. That’s why I built it.

You can also decide if you want to assign some sort of scoring protocol. For example, a player who solves the word in one or two guesses may get a higher score than if it took them five or six guesses.

For now, our example will be win/lose, but here’s a nice idea if you’re so inclined:

First guess: 1000pts

Second guess: 200pts

Third guess: 100pts

Fourth guess: 60pts

Fifth guess: 45 points

Sixth guess: 15points

Talk about incentive.

Now we have our idea, let’s build our app.

Step 2: Build the App

Next, we want to create the framework for our app. We’ll be doing our editing in VSCode. So let’s just sashay over to git and setup a react app like this:

npx create-react-app Wordle

Disclaimer: you’re probably going to have to come up with some different name, and I have to warn you, there’s about 11-thousand versions of this game, all having some ‘word’ name. Get creative.

Now we want to go in and gut out all the stuff we don’t need. For this purpose I usually go right in and delete the images that come with the app; logo, favicon, etc. You’ll find them in the ‘public’ folder.

I’m assuming you have some experience with ReactJS, so I’m not going to dumb this tutorial down too much, but here’s something like what you should see:

A ReactJS App Framework

Yours won’t look exactly like this, because this screenshot was taken after I had already messed with it some, but those two ‘.png’ images in the public folder can get cut right out. You’ll also want to remove references to them in the code, but the beauty is that it won’t compile if you don’t — and will take you right to them.

The favicon is my favorite. That’s the little picture on the tab when you’re looking at a webpage. It’s also the icon used when many other sites refer to your app. I like to make my own and save it under the same name “favicon.ico” in the public folder. That’s literally all you have to do with that.

You can also see a file called “5LetterWords.txt” in that same folder. That’s the database (it’s a text file) we’ll be using.

You didn’t think we were going to sit here and make up 5000 words, did you? Nope. We look around online until we find a dictionary that contains only 5-letter words, and lots of them. I found this one at Github.

Simply save that file to the public directory. We put it there so that we can access it by using a fetch command later.

Consideration: while you might be inclined to build an ExpressJS backend with an API that you can call to retrieve a word and to check the player’s words for correctness, since we’ll be hosting this app at heroku.com, it’s much easier just to store the file locally instead of having to “wake up” a whole nother app for the API.

If later you want to add login functionality, recordkeeping and maybe leaderboards and such, an ExpressJS API would be the way to go.

Step 3: Code the App

Setting Our State Variables

Most of our figuring is going to be done in the App.js file. This is a really simple app, so it won’t even be 200 lines of code — or not many more, if that.

Let’s start by setting up our state variables. Here are the ones I chose, and we’ll talk about them in a minute:

this.state = {words:[],board:[[{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"}],[{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"}],[{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"}],[{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"}],[{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"}],[{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"},{letter:"",status:"white"}]],currentRow:0,currentWord: '',currentGuess: '',errorMessage: String.fromCharCode(160),gameOver: false};}

The “words” array will hold the entire dictionary. It’s only 5,000 words, so pretty negligible as far as resource management goes. Like I said, given better hosting options, all that would be done through an API, so you’d only ever have one word in memory at a time.

Next is the board itself. It’s an array of six arrays. Each of the six arrays is a row of the board, and will simply hold the letter chosen and the “status” of that letter (found, found but in the wrong spot, or just completely wrong).

Of course you could abstract all that in a hundred different ways, but it’s a simple, non-changing playing board that’s very small, so it’s way easier to just draw it right out.

Of course, the next variable “currentRow” tells the app which turn the player is taking. In this case it starts at 0 and goes to 5 — so six turns.

The following variables, “currentWord” and “currentGuess” will hold the word the app chose as the target word, and whatever word the player is currently trying to play.

We use the “errorMessage” variable as a string to send messages to the player; ie: “that word’s too long,” “there’s no such word in the dictionary” (for when they try to cheat by just mashing random letters), “You Win!”, “You lose” and whatever else we want to communicate to the player.

You might notice this default value for “errorMessage”:

String.fromCharCode(160)

That’s a hard space, so that whenever there’s no message being displayed, the layout will still accommodate the position of the message on the screen. That’s just so that when we do output a message there, it won’t cause all the other display elements to move around.

In this image, it’s the words in red:

If that was usually blank, everything would move around when there were words there.

And finally, we have our “gameOver” variable. That’s to tell the game whether or not to accept more input from the player.

Loading Our Dictionary

Now we want to load our dictionary into the “words” array. Then we can choose a random word for our target word, and also use that array to compare the player’s guess to see if it’s a valid word or just random keypresses.

async downloadDictionary() {let response = await fetch("5LetterWords.txt");if(response.status !== 200) {throw new Error("Server Error");}// read response stream as textlet text_data = await response.text();let wordList = text_data.split("\n");let maxWords = wordList.length;let wordNumber = getRandomInt(0,maxWords);this.setState({words: wordList,currentWord:wordList[wordNumber].toUpperCase()});}

That’s the whole function. First, we fetch the word list, then we split it into an array with this line:

let wordList = text_data.split("\n");

That divides the text file up between each line of the file and makes each line an element of the array, “wordlist.” Then we’re going to pick a random number somewhere between 0 and however many words are in the dictionary like this:

let maxWords = wordList.length;let wordNumber = getRandomInt(0,maxWords);

Which, of course by now we’ve already visited stackoverflow and gotten our handy random-number generator:

function getRandomInt(min, max) {min = Math.ceil(min);max = Math.floor(max);return Math.floor(Math.random() * (max - min + 1)) + min;}

I put that outside the App class, so it can be called from anywhere, even though it won’t be.

Now that we’ve chosen our random word, we’re going to store the entire wordlist, and that chosen word into our state variable:

this.setState({words: wordList,currentWord:wordList[wordNumber].toUpperCase()});

Here’s the important part — when do we call this function?

We call it when the App component first mounts, like this:

componentDidMount = () => {this.downloadDictionary();}

Let’s Draw Our Board!

Finally, we get to see some output. Let’s build the empty playing board, so we can get to see some action happening on the screen.

Here’s the “render” portion of the App component. We’ll talk about it after:

render(){console.log(this.state.currentWord);return (<div className="App"><header className="App-header"><Header errorMessage={this.state.errorMessage}/><div style={{width:'180px'}}>{this.state.board.map((board,idx)=>{return (<div key = {idx}>{board.map((row,idx)=>{return(<div><Tile letter={row.letter} color={row.status} key={idx}/></div>)})}</div>)})}</div><input type="text" id="guessWordBox" value={this.state.currentGuess} maxlength="5" className='inputBox' /><Keyboard keyboardType={this.keyboardType} /></header></div>);}

The first thing you’ll notice is our secret hidden Easter egg, the console.log(this.state.currentWord) part.

If at any time in the game you start feeling really dumb, you can just press f12 to open the console, and the App’s chosen word will be sitting right there to make you feel good and inferior for not figuring it out yourself.

I’ve looked through that dictionary, and you’d be amazed how many valid 5-letter words just look like a random string of letters. I mean, “taros” — -really?

We’re just sticking with the default app and header styles:

<div className="App"><header className="App-header">

but then we’ll be adding our own header component to show the game title and error message.

For this we’ll start a components folder and add a file called “Header.js”, so your folder structure should look something like this:

We haven’t added Keyboard or Tile yet, but we will

The Header component is super-simple, so here’s the whole file:

import React, { Component } from 'react';class Header extends Component {render() {return (<div><div style={{fontSize:'2.5em'}}> 🆆 🅾 🆁 🅳 🅻 🅴 🆁 <br/></div><div style={{color:'red'}}>{this.props.errorMessage}</div></div>)}}export default Header;

Literally all it does is display the name of the game and the errorMessage variable, which is sent to it as a prop from App.js:

<Header errorMessage={this.state.errorMessage}/>

This way, whenever we change the error message in the state variable in App.js, it will automatically propagate to the Header component.

Now we’re getting to some fun

In our App component, we’re going to map through the board array, which contains six other arrays that we also have to map through. So we’re going to need to nest our mapping functions, like this:

{this.state.board.map((board,idx)=>{return (<div key = {idx}>{board.map((row,idx)=>{return(<div><Tile letter={row.letter} color={row.status} key={idx}/></div>)})}

What we’re doing there is mapping through each of the six rows that make up the board, and for each row, we’re going to map every tile in that row. Remember the tiles contain just whatever letter the player typed and the color the tile is supposed to be.

When we first start the game, of course, all the variables in that array show just a blank space for the letter and “white” for the color. So we should just display six rows of five blank tiles.

Now let’s define the tiles

We need a new file in our components folder, which we’ll call “Tile.js” that will describe each tile of the game board. This is also a really simple component, so once again, here’s the whole file:

import React, { Component } from 'react';class Tile extends Component {render() {let bgStyle= {width:'30px',backgroundColor:this.props.color,height:'30px',fontSize:'x-large',float:'left',border:'1px solid white',marginRight:'4px',marginTop:'4px',paddingBottom:'4px',color:"black",borderRadius:'5px',verticalAlign:'middle'}return (<div style={bgStyle}>{this.props.letter}</div>)}}export default Tile;

All we’re doing here is describing the css style of each tile, but keeping the “backgoundColor” setting as a variable, which will be passed through as the prop “color” in this line:

<Tile letter={row.letter} color={row.status} key={idx}/>

Remember, those props come from the “board” array in our App.js state variable.

Let’s build our own keyboard, too!

You may have noticed from the dimensions of our playing board that this app is supposed to be playable on a mobile phone, not just on a PC. That’s why everything’s kept in such a narrow area in the center of the screen.

So we’re also going to build a little keyboard for the player, so they don’t have to keep popping their phone keyboard on and off every time.

The next two lines in App.js show what I mean. We need an input box to display the word as we type it and a keyboard to type it on:

<input type="text" id="guessWordBox" value={this.state.currentGuess} maxlength="5" className='inputBox' /><Keyboard keyboardType={this.keyboardType} usedList={this.state.usedList} foundList={this.state.foundList} correctList={this.state.correctList} />

The input box will be called “guessWordBox” and will hold the “currentGuess” variable from the App’s state. So it starts out empty.

Then comes the keyboard. Once again, really simple, so here’s the whole file:

import React, { Component } from 'react';import '../App.css';class Keyboard extends Component {constructor(props) {super(props);this.state = {topRow:["Q","W","E","R","T","Y","U","I","O","P"],middleRow:["A","S","D","F","G","H","J","K","L"],bottomRow:["Z","X","C","V","B","N","M","←","Enter"]};}handleClick = (event) => {this.props.keyboardType(event.currentTarget.textContent);};
render() {return (<div style={{display: "flex",flexWrap:"wrap",justifyContent:"center",width:"330px",textAlign:"center"}}><div style={{display: "flex",flexWrap:"wrap",justifyContent:"center",width:"100%"}}>{this.state.topRow.map((letter,idx)=> <div className='keyStyle' key={letter} onClick={this.handleClick} >{letter}</div>)})}</div> <br/><div style={{display: "flex",flexWrap:"wrap",justifyContent:"center",width:"100%"}}>{this.state.middleRow.map((letter,idx)=> {return(<div className='keyStyle' key={letter} onClick={this.handleClick} >{letter}</div>)})}</div><br/><div style={{display: "flex",flexWrap:"wrap",justifyContent:"center",width:"100%"}}>{this.state.bottomRow.map((letter,idx)=> <div className='keyStyle' key={letter} onClick={this.handleClick} >{letter}</div>)})}</div></div>)}}export default Keyboard;

You can see in our state variable that we define the keyboard as three rows, to match the rows on a standard typewriter (are those still a thing?).

this.state = {topRow:["Q","W","E","R","T","Y","U","I","O","P"],middleRow:["A","S","D","F","G","H","J","K","L"],bottomRow:["Z","X","C","V","B","N","M","←","Enter"]};}

Here’s the important part:

handleClick = (event) => {this.props.keyboardType(event.currentTarget.textContent);};

When we call the Keyboard component from our app, we send it a prop to the function to trigger whenever someone types on it. Since the function is in the App component, we will pass the typed key back to App to be processed there.

The value of the key being pressed is found in event.currentTarget.textContent, because the “event” being referenced is the “onClick” event, and that letter is what we’re sending back to App.js is the content of the DIV on the keyboard.

Here’s a reminder of how we called it from our App:

<Keyboard keyboardType={this.keyboardType} } />

The “keyboardType” prop is a reference to the keyboardType function (that we haven’t written yet, but will soon).

All we’re doing next is to draw each row of the keyboard. To do that, we just map through the array in the state variable, like this:

{this.state.topRow.map((letter,idx)=><div className='keyStyle' key={letter} onClick={this.handleClick} >{letter}</div>)})}</div>

It’s just a bunch of DIVs that are clickable (onClick), with each one containing the letter assigned to it in the array: {letter}.

The onClick function calls the handleClick function we defined earlier, whose sole purpose is to just send the clicked letter back to our App.js component for processing.

So let’s get to processing them clicks

In our App.js file we rendered the keyBoard component with a prop called:

<Keyboard keyboardType={this.keyboardType} />

This refers to a function called “keyboardType”, which is right here:

keyboardType = (keyClicked) => {this.setState({errorMessage:String.fromCharCode(160)})let errorMessage = String.fromCharCode(160);let currentGuess= '';let guessBox = document.getElementById("guessWordBox");if(guessBox) {if(keyClicked==="Enter" && guessBox.value.length>=5){this.submitWord(guessBox.value);} else {if(guessBox.value.length<=5){if(keyClicked==="←"){guessBox.value = guessBox.value.substring(0, guessBox.value.length - 1);currentGuess=guessBox.value;}elseguessBox.value+=keyClicked;currentGuess=guessBox.value;}else {errorMessage="We're only looking for 5-digit words";currentGuess=''this.setState({errorMessage:errorMessage})}}}

You’ll remember “keyClicked” is the letter we sent back from the Keyboard component when the player presses one of the keys:

keyboardType = (keyClicked) => {

The first thing we do here is to clear out whatever previous message was being displayed to the user:

this.setState({errorMessage:String.fromCharCode(160)})

This makes it so they won’t still be seeing “that’s not a word” or something when typing a new word.

We also set our local default variables that will only be used in this function:

let errorMessage = String.fromCharCode(160);let currentGuess= '';

Next we’re going to check the input box to see what word is there, if there is one:

let guessBox = document.getElementById("guessWordBox");if(guessBox) {

We check to see if the person has pressed the “Enter” button and if they‘ve typed a word that’s long enough or not. If it’s legit, then we’re going to call the “submitWord” function:

if(keyClicked==="Enter" && guessBox.value.length>=5){this.submitWord(guessBox.value);}

We haven’t written “submitWord” yet, but that’ll be the function that compares the player’s guess to the App’s target word. Basically, checking to see if they won.

Otherwise, if they’re just pressing the left-arrow (backspace) button, we’ll get rid of whatever letter they typed last:

else {if(guessBox.value.length<=5){if(keyClicked==="←"){guessBox.value = guessBox.value.substring(0, guessBox.value.length - 1);currentGuess=guessBox.value;}

And if they’re still typing, and have gone over five letters, we’ll send them a reminder:

else {errorMessage="We're only looking for 5-digit words";currentGuess=''this.setState({errorMessage:errorMessage})}

That can be tidied up and streamlined a lot. That’s a technique called “refactoring,” where we get code that works and then make it more efficient and comprehensive after.

Let’s see if they won

The “submitWord” function has a lot going on. Here it is:

submitWord = (wordSubmitted) => {if(!this.state.gameOver){if (this.checkword(wordSubmitted)){let currentBoard = [...this.state.board];let currentWord = this.state.currentWord;let boardRow = this.state.currentRow;let errorMessage= this.state.errorMessage;let tileColor="white";let newRow = [];for(let i =0; i<wordSubmitted.length;i++){if(currentWord.includes(wordSubmitted[i])){tileColor="blue";if(currentWord[i]===wordSubmitted[i]){tileColor="lightgreen";}}newRow[i]={letter: wordSubmitted[i],status: tileColor}tileColor="white";}currentBoard[boardRow]=newRow;boardRow++;let gameOver = falseif(boardRow>5) {gameOver=true;errorMessage="Bad Luck. The word was "+ currentWord + ". Play again!";}if(wordSubmitted===currentWord){errorMessage="You WIN!"gameOver=true;}this.setState({board:currentBoard,currentRow:boardRow,gameOver:gameOver,errorMessage: errorMessage})}}}

The very first thing we do is see if the game is over or not:

submitWord = (wordSubmitted) => {if(!this.state.gameOver){

If it’s not over, then we check to see if our dictionary even contains the word the player entered:

if (this.checkword(wordSubmitted)){

Here’s the checkWord function:

checkword = (wordToCheck) => {let wordList = this.state.words;let word = wordToCheck.toLowerCase();if (wordList.includes(word)){return(true);}else {this.setState({errorMessage:"That word is not in my dictionary"})}}

All we’re doing here is checking the “words” array in our state variable to see if whatever they typed in the box is a valid word.

Note: it’s possible the word they typed is a real word, but if it’s not in our dictionary it couldn’t possibly be the target word anyway.

If the word turns out to be valid, then we can continue on with the function. First, we’re going to set up our local variables:

let currentBoard = [...this.state.board];let currentWord = this.state.currentWord;let boardRow = this.state.currentRow;let errorMessage= this.state.errorMessage;let tileColor="white";let newRow = [];

What we’re doing here is getting all the relevant state variables and storing them in local variables.

We do this because our state variable is “immutable,” meaning it can’t be changed on the fly, but rather the variables must be set to the specific value you want them to be.

So you can’t do something like

this.setState({variable: variable + 1;})

Instead, you would do something like:

let tempVariable = this.state.variable + 1this.setState({variable: tempVariable})

It’s a whole big thing, but not that difficult. The compiler won’t let you forget, either.

The only thing in that variable list that is new is “newRow.” That will be the row on the board that will contain whatever word the player typed.

What we’re going to do is go through each letter in the word the player typed, one-by-one.

We’ll check to see if it exists in the target word, and if it does, highlight it in blue. We’ll also check to see if it exists in the same spot in the target word relative to the position in the player’s word. If so, we’ll highlight it in green, which will override the blue.

for(let i =0; i<wordSubmitted.length;i++){if(currentWord.includes(wordSubmitted[i])){tileColor="blue";if(currentWord[i]===wordSubmitted[i]){tileColor="lightgreen";}}newRow[i]={letter: wordSubmitted[i],status: tileColor}}

You see there at the end where we put the letter and its color into the “newRow” array. That will later be the current row on the board.

Next we replace the row in our “board” state variable with the row that the player just typed:

currentBoard[boardRow]=newRow;boardRow++;let gameOver = false

We increment the “boardRow” variable to show that we’re moving to the next turn, and set a variable to represent whether the game’s ended or not. Which we’ll address now. It’s pretty straightforward:

if(boardRow>5) {gameOver=true;errorMessage="Bad Luck. The word was "+ currentWord + ". Play again!";}if(wordSubmitted===currentWord){errorMessage="You WIN!"gameOver=true;}

If the player’s used more than six rows (row 0 to row 5), they lost. But if they guessed the word correctly, then they’ve WON! And here we set the variables appropriately for those conditions.

Keep in mind, we’ve been juggling a lot of variables in this function, but we haven’t done anything with the state yet. Thus, all those changes are only imaginary, until we manifest them in the App’s state variable, which we do now:

this.setState({board:currentBoard,currentRow:boardRow,gameOver:gameOver,errorMessage: errorMessage})}

We set the “board” variable to the local “currentBoard” array — in which we’ve just set the “newRow” array to replace whatever row the player’s currently playing.

We increment the “currentRow” variable to tell that it’s coming up on a new turn (or end of game if they’ve played all their turns already).

We update both our “gameOver” and “errorMessage” variables with their new values, or the default values we assigned them at the beginning of the function.

And go on to our next turn.

That’s it. That’s the whole game. I know it can seem confusing looking at all these functions and components as snippets — not to mention all the jumping back and forth. Don’t worry, I’m going to add a link to the github repo, the dictionary file, and a live version of the game at the end here, so you can see them all working together for real.

Step 5: Upgrades, People, Upgrades

Now it’s all yours to play with as you like. Feel free to fork my repo if you want a head start.

You can create a database for player records so they can track their progress, you can build an API to deliver words from the dictionary and check the validity of other words. You can design a leaderboard to show the top players.

Or you can just change the colors and the layout. It’s all up to you.

Happy Hacking!

=======================

The Live Version: https://wordlier.netlify.app/

Dictionary file: https://github.com/charlesreid1/five-letter-words/blob/master/sgb-words.txt

The Github Repo: https://github.com/crashdaddy/wordlier

Plus, I like coffee

--

--

crashdaddy

4a 75 73 74 20 61 6e 6f 74 68 65 72 20 63 6f 6d 70 75 74 65 72 20 6e 65 72 64 20 77 69 74 68 20 61 20 62 6c 6f 67