DEV Community

Spencer Carli
Spencer Carli

Posted on • Originally published at reactnativeschool.com

Build a Wordle Clone with React Native

This post was originally published on React Native School.

Wordle has become an increasingly popular game and it’s one that we can build with React Native... in a single file!

Here’s a demo of what we’ll be building today.

Demo of the React Native Wordle clone

Let’s jump in, shall we?

We'll use Expo and TypeScript throughout this tutorial. Feel free to use whatever you like - nothing is Expo specific.

To create a new Expo project with TypeScript running the following commands

expo init WordleClone
Enter fullscreen mode Exit fullscreen mode

Then select “blank (TypeScript)” under Managed Workflow.

Build the Wordle UI with React Native

First we’ll build the UI and then add functionality. We’ll use a SafeAreaView so all our content is visible and not hidden by any notches potentially on a device.

import React from 'react';
import { StyleSheet, View, SafeAreaView } from 'react-native';

export default function App() {
  return (
    <SafeAreaView>
      <View></View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({});
Enter fullscreen mode Exit fullscreen mode

Guess Blocks

In Wordle you have 6 chances to guess a 5 letter word. We’ll first build out the grid to display our guesses.

Grid of 6 rows and 6 columns

import React from 'react';
import { StyleSheet, View, SafeAreaView } from 'react-native';

const GuessRow = () => (
  <View style={styles.guessRow}>
    <View style={styles.guessSquare}></View>
    <View style={styles.guessSquare}></View>
    <View style={styles.guessSquare}></View>
    <View style={styles.guessSquare}></View>
    <View style={styles.guessSquare}></View>
  </View>
);

export default function App() {
  return (
    <SafeAreaView>
      <View>
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  guessRow: {
    flexDirection: 'row',
    justifyContent: 'center',
  },
  guessSquare: {
    borderColor: '#d3d6da',
    borderWidth: 2,
    width: 50,
    height: 50,
    alignItems: 'center',
    justifyContent: 'center',
    margin: 5,
  },
});
Enter fullscreen mode Exit fullscreen mode

We need to then display the letters within each “block”. Since there is a decent amount of duplication (and it’s only going to increase) I’m going to break the block into a component.

Grid with placeholder letters

import React from 'react';
import { StyleSheet, View, SafeAreaView, Text } from 'react-native';

const Block = ({ letter }: { letter: string }) => (
  <View style={styles.guessSquare}>
    <Text style={styles.guessLetter}>{letter}</Text>
  </View>
);

const GuessRow = () => (
  <View style={styles.guessRow}>
    <Block letter="A" />
    <Block letter="E" />
    <Block letter="I" />
    <Block letter="O" />
    <Block letter="" />
  </View>
);

// ...

const styles = StyleSheet.create({
  // ...
  guessLetter: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#878a8c',
  },
});
Enter fullscreen mode Exit fullscreen mode

Keyboard

Rather than using the system keyboard we’ll build our own simple one, following the QWERTY layout.

Each key should be tappable and we’ll rely on React Native’s Flexbox implementation to figure out the size of each key.

Keyboard layout

import React from 'react';
import {
  StyleSheet,
  View,
  SafeAreaView,
  Text,
  TouchableOpacity,
} from 'react-native';

// ...

const Keyboard = () => {
  const row1 = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'];

  return (
    <View style={styles.keyboard}>
      <View style={styles.keyboardRow}>
        {row1.map((letter) => (
          <TouchableOpacity>
            <View style={styles.key}>
              <Text style={styles.keyLetter}>{letter}</Text>
            </View>
          </TouchableOpacity>
        ))}
      </View>
    </View>
  );
};

export default function App() {
  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
      </View>
      <Keyboard />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  // ...

  container: {
    justifyContent: 'space-between',
    flex: 1,
  },

  // keyboard
  keyboard: { flexDirection: 'column' },
  keyboardRow: {
    flexDirection: 'row',
    justifyContent: 'center',
    marginBottom: 10,
  },
  key: {
    backgroundColor: '#d3d6da',
    padding: 10,
    margin: 3,
    borderRadius: 5,
  },
  keyLetter: {
    fontWeight: '500',
    fontSize: 15,
  },
});
Enter fullscreen mode Exit fullscreen mode

We can then leverage our KeyboardRow component to fill out the next two levels of the keyboard. On the third row I’m using to represent the delete key.

Full keyboard layout

// ...

const KeyboardRow = ({ letters }: { letters: string[] }) => (
  <View style={styles.keyboardRow}>
    {letters.map((letter) => (
      <TouchableOpacity>
        <View style={styles.key}>
          <Text style={styles.keyLetter}>{letter}</Text>
        </View>
      </TouchableOpacity>
    ))}
  </View>
);

const Keyboard = () => {
  const row1 = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'];
  const row2 = ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'];
  const row3 = ['Z', 'X', 'C', 'V', 'B', 'N', 'M', ''];

  return (
    <View style={styles.keyboard}>
      <KeyboardRow letters={row1} />
      <KeyboardRow letters={row2} />
      <KeyboardRow letters={row3} />
    </View>
  );
};

// ...
Enter fullscreen mode Exit fullscreen mode

And finally we’ll add an “ENTER” key so the user can submit their guess.

Keyboard with enter

// ...

const Keyboard = () => {
  const row1 = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'];
  const row2 = ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'];
  const row3 = ['Z', 'X', 'C', 'V', 'B', 'N', 'M', ''];

  return (
    <View style={styles.keyboard}>
      <KeyboardRow letters={row1} />
      <KeyboardRow letters={row2} />
      <KeyboardRow letters={row3} />
      <View style={styles.keyboardRow}>
        <TouchableOpacity>
          <View style={styles.key}>
            <Text style={styles.keyLetter}>ENTER</Text>
          </View>
        </TouchableOpacity>
      </View>
    </View>
  );
};

// ...
Enter fullscreen mode Exit fullscreen mode

Capture User Input

Right now our keys don’t do anything. We’ll create a function that we pass down to each key that will be called each time a user taps one of the keys in our keyboard.

Alert of letter pressed

// ...

const KeyboardRow = ({
  letters,
  onKeyPress,
}: {
  letters: string[],
  onKeyPress: (letter: string) => void,
}) => (
  <View style={styles.keyboardRow}>
    {letters.map((letter) => (
      <TouchableOpacity onPress={() => onKeyPress(letter)}>
        <View style={styles.key}>
          <Text style={styles.keyLetter}>{letter}</Text>
        </View>
      </TouchableOpacity>
    ))}
  </View>
);

const Keyboard = ({ onKeyPress }: { onKeyPress: (letter: string) => void }) => {
  const row1 = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'];
  const row2 = ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'];
  const row3 = ['Z', 'X', 'C', 'V', 'B', 'N', 'M', ''];

  return (
    <View style={styles.keyboard}>
      <KeyboardRow letters={row1} onKeyPress={onKeyPress} />
      <KeyboardRow letters={row2} onKeyPress={onKeyPress} />
      <KeyboardRow letters={row3} onKeyPress={onKeyPress} />
      <View style={styles.keyboardRow}>
        <TouchableOpacity onPress={() => onKeyPress('ENTER')}>
          <View style={styles.key}>
            <Text style={styles.keyLetter}>ENTER</Text>
          </View>
        </TouchableOpacity>
      </View>
    </View>
  );
};

export default function App() {
  const handleKeyPress = (letter: string) => {
    alert(letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
        <GuessRow />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Enter fullscreen mode Exit fullscreen mode

Before we actually start capturing user key input lets remove the placeholder text.

Grid without placeholder letters

// ...

const GuessRow = () => (
  <View style={styles.guessRow}>
    <Block letter="" />
    <Block letter="" />
    <Block letter="" />
    <Block letter="" />
    <Block letter="" />
  </View>
);

// ...
Enter fullscreen mode Exit fullscreen mode

Capture & Store Letter Presses

Now we can capture the actually keys a user presses. We’ll store a user’s guess in React state and then pass that state down to the GuessRow component. We’ll handle multiple guesses later.

Grid with user selected letters displayed

// ...

const GuessRow = ({ guess }: { guess: string }) => {
  const letters = guess.split('');

  return (
    <View style={styles.guessRow}>
      <Block letter={letters[0]} />
      <Block letter={letters[1]} />
      <Block letter={letters[2]} />
      <Block letter={letters[3]} />
      <Block letter={letters[4]} />
    </View>
  );
};

// ...

export default function App() {
  const [guess, setGuess] = React.useState('');

  const handleKeyPress = (letter: string) => {
    setGuess(guess + letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guess} />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Enter fullscreen mode Exit fullscreen mode

Handle a Complete Guess

A guess is considered complete if it is 5 characters long. We don’t want to continue to add characters to a string of 5 characters.

Grid with user selected letters displayed

// ...

export default function App() {
  const [guess, setGuess] = React.useState('');

  const handleKeyPress = (letter: string) => {
    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuess(guess + letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guess} />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Enter fullscreen mode Exit fullscreen mode

Handle Delete Tap

Another consideration we need to make is when the delete key is pressed. We should delete the last character from the string, which can be accomplished using a string slice.

Grid with user selected letters displayed

// ...

export default function App() {
  const [guess, setGuess] = React.useState('');

  const handleKeyPress = (letter: string) => {
    if (letter === '') {
      setGuess(guess.slice(0, -1));
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuess(guess + letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guess} />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Enter fullscreen mode Exit fullscreen mode

Handle Enter Tap

When a user presses “enter” we have a few considerations to make. First is if the word is too short (length less than 5).

Word too short warning

// ...

export default function App() {
  const [guess, setGuess] = React.useState('');

  const handleKeyPress = (letter: string) => {
    if (letter === 'ENTER') {
      if (guess.length !== 5) {
        alert('Word too short.');
        return;
      }
    }

    if (letter === '') {
      setGuess(guess.slice(0, -1));
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuess(guess + letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guess} />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Enter fullscreen mode Exit fullscreen mode

We also need to check if the word they submitted exists in our dictionary. This prevents people from mining for words by submitting something like “aeiou” in search of vowels.

Word not in dictionary warning

// ...

const words = ['LIGHT', 'WRUNG', 'COULD', 'PERKY', 'MOUNT', 'WHACK', 'SUGAR'];

export default function App() {
  const [guess, setGuess] = React.useState('');

  const handleKeyPress = (letter: string) => {
    if (letter === 'ENTER') {
      if (guess.length !== 5) {
        alert('Word too short.');
        return;
      }

      if (!words.includes(guess)) {
        alert('Not a valid word.');
        return;
      }
    }

    if (letter === '') {
      setGuess(guess.slice(0, -1));
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuess(guess + letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guess} />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Enter fullscreen mode Exit fullscreen mode

Finally, lets check if the word they submitted is equal to the active word.

Win message

// ...

export default function App() {
  const [activeWord] = React.useState(words[0]);
  const [guess, setGuess] = React.useState('');

  const handleKeyPress = (letter: string) => {
    if (letter === 'ENTER') {
      if (guess.length !== 5) {
        alert('Word too short.');
        return;
      }

      if (!words.includes(guess)) {
        alert('Not a valid word.');
        return;
      }

      if (guess === activeWord) {
        alert('You win!');
        return;
      }
    }

    if (letter === '') {
      setGuess(guess.slice(0, -1));
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuess(guess + letter);
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guess} />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
        <GuessRow guess="" />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Enter fullscreen mode Exit fullscreen mode

Manage State of Multiple Guesses

Now we need to do the more complicated part - handling the (up to) 6 guesses a user can make.

We’ll do this by creating a guesses object and a guess index. When a user submits a valid guess we’ll increase our guess index by one.

We can pass each guess down to the relevant GuessRow to be displayed.

Finally, we need to consider if they’ve reached their game guess limit when pressing submit.

Display multiple guesses in grid

// ...

interface IGuess {
  [key: number]: string;
}

const defaultGuess: IGuess = {
  0: '',
  1: '',
  2: '',
  3: '',
  4: '',
  5: '',
};

export default function App() {
  const [activeWord] = React.useState(words[0]);
  const [guessIndex, setGuessIndex] = React.useState(0);
  const [guesses, setGuesses] = React.useState < IGuess > defaultGuess;

  const handleKeyPress = (letter: string) => {
    const guess: string = guesses[guessIndex];

    if (letter === 'ENTER') {
      if (guess.length !== 5) {
        alert('Word too short.');
        return;
      }

      if (!words.includes(guess)) {
        alert('Not a valid word.');
        return;
      }

      if (guess === activeWord) {
        alert('You win!');
        return;
      }

      if (guessIndex < 5) {
        setGuessIndex(guessIndex + 1);
      } else {
        alert('You lose!');
      }
    }

    if (letter === '') {
      setGuesses({ ...guesses, [guessIndex]: guess.slice(0, -1) });
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuesses({ ...guesses, [guessIndex]: guess + letter });
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow guess={guesses[0]} />
        <GuessRow guess={guesses[1]} />
        <GuessRow guess={guesses[2]} />
        <GuessRow guess={guesses[3]} />
        <GuessRow guess={guesses[4]} />
        <GuessRow guess={guesses[5]} />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

// ...
Enter fullscreen mode Exit fullscreen mode

Indicate Accuracy in a Guess Block

So far we’ve got no visual indication if a guess has correctly placed letters or if they’re in the word.

To determine if a letter is in the word or in the correct place we need to know:

  1. Has the user submitted that guess yet (we don’t want to show them if a letter is correct before they press “ENTER”)
  2. What is the word the user is trying to guess?
  3. What is the user’s guess?

By passing that information down to the Block component we can first compare if the letter the user guessed is the same as the letter in the word at that index.

If so, we should make the text white and the background green.

In the following image the word the user is trying to guess is “LIGHT” and their guess was “TIGHT”

Indicate correctly placed letters

// ...

const Block = ({
  index,
  guess,
  word,
  guessed,
}: {
  index: number,
  guess: string,
  word: string,
  guessed: boolean,
}) => {
  const letter = guess[index];
  const wordLetter = word[index];

  const blockStyles: any[] = [styles.guessSquare];
  const textStyles: any[] = [styles.guessLetter];

  if (letter === wordLetter && guessed) {
    blockStyles.push(styles.guessCorrect);
    textStyles.push(styles.guessedLetter);
  }

  return (
    <View style={blockStyles}>
      <Text style={textStyles}>{letter}</Text>
    </View>
  );
};

const GuessRow = ({
  guess,
  word,
  guessed,
}: {
  guess: string,
  word: string,
  guessed: boolean,
}) => {
  return (
    <View style={styles.guessRow}>
      <Block index={0} guess={guess} word={word} guessed={guessed} />
      <Block index={1} guess={guess} word={word} guessed={guessed} />
      <Block index={2} guess={guess} word={word} guessed={guessed} />
      <Block index={3} guess={guess} word={word} guessed={guessed} />
      <Block index={4} guess={guess} word={word} guessed={guessed} />
    </View>
  );
};

// ...

const words = [
  'LIGHT',
  'TIGHT',
  'WRUNG',
  'COULD',
  'PERKY',
  'MOUNT',
  'WHACK',
  'SUGAR',
];

interface IGuess {
  [key: number]: string;
}

const defaultGuess: IGuess = {
  0: '',
  1: '',
  2: '',
  3: '',
  4: '',
  5: '',
};

export default function App() {
  const [activeWord] = React.useState(words[0]);
  const [guessIndex, setGuessIndex] = React.useState(0);
  const [guesses, setGuesses] = React.useState < IGuess > defaultGuess;

  const handleKeyPress = (letter: string) => {
    // ...
  };

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow
          guess={guesses[0]}
          word={activeWord}
          guessed={guessIndex > 0}
        />
        <GuessRow
          guess={guesses[1]}
          word={activeWord}
          guessed={guessIndex > 1}
        />
        <GuessRow
          guess={guesses[2]}
          word={activeWord}
          guessed={guessIndex > 2}
        />
        <GuessRow
          guess={guesses[3]}
          word={activeWord}
          guessed={guessIndex > 3}
        />
        <GuessRow
          guess={guesses[4]}
          word={activeWord}
          guessed={guessIndex > 4}
        />
        <GuessRow
          guess={guesses[5]}
          word={activeWord}
          guessed={guessIndex > 5}
        />
      </View>
      <Keyboard onKeyPress={handleKeyPress} />
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  // ...
  guessedLetter: {
    color: '#fff',
  },
  guessCorrect: {
    backgroundColor: '#6aaa64',
    borderColor: '#6aaa64',
  },

  container: {
    justifyContent: 'space-between',
    flex: 1,
  },

  // ...
});
Enter fullscreen mode Exit fullscreen mode

Next case to consider is if the letter is in the word but at a different index. We should only check this if the character in the guess and character in the word do not match.

Again, the correct word is “LIGHT” but the user guessed “GOING”

Indicate incorrectly placed letters

// ...

const Block = ({
  index,
  guess,
  word,
  guessed,
}: {
  index: number,
  guess: string,
  word: string,
  guessed: boolean,
}) => {
  const letter = guess[index];
  const wordLetter = word[index];

  const blockStyles: any[] = [styles.guessSquare];
  const textStyles: any[] = [styles.guessLetter];

  if (letter === wordLetter && guessed) {
    blockStyles.push(styles.guessCorrect);
    textStyles.push(styles.guessedLetter);
  } else if (word.includes(letter) && guessed) {
    blockStyles.push(styles.guessInWord);
    textStyles.push(styles.guessedLetter);
  }

  return (
    <View style={blockStyles}>
      <Text style={textStyles}>{letter}</Text>
    </View>
  );
};

// ...
Enter fullscreen mode Exit fullscreen mode

Finally we’ll indicate to the user if a letter they guessed is not in the word at all by setting the background to grey, ruling that letter out for future guesses.

Once again the correct word is “LIGHT” and the user guess is “GOING.

Indicate letters not in word

// ...

const Block = ({
  index,
  guess,
  word,
  guessed,
}: {
  index: number,
  guess: string,
  word: string,
  guessed: boolean,
}) => {
  const letter = guess[index];
  const wordLetter = word[index];

  const blockStyles: any[] = [styles.guessSquare];
  const textStyles: any[] = [styles.guessLetter];

  if (letter === wordLetter && guessed) {
    blockStyles.push(styles.guessCorrect);
    textStyles.push(styles.guessedLetter);
  } else if (word.includes(letter) && guessed) {
    blockStyles.push(styles.guessInWord);
    textStyles.push(styles.guessedLetter);
  } else if (guessed) {
    blockStyles.push(styles.guessNotInWord);
    textStyles.push(styles.guessedLetter);
  }

  return (
    <View style={blockStyles}>
      <Text style={textStyles}>{letter}</Text>
    </View>
  );
};

// ...
Enter fullscreen mode Exit fullscreen mode

Reset Game

Unlike the real Wordle game, where you can only play once per day, we’ll set ours up so that a user can reset the game and get a new word after winning/losing the game.

When the game is reset we’ll reset all the values by listening to the changing state using a useEffect.

Reset game button

import React from 'react';
import {
  StyleSheet,
  View,
  SafeAreaView,
  Text,
  TouchableOpacity,
  Button,
} from 'react-native';

// ...

export default function App() {
  const [activeWord, setActiveWord] = React.useState(words[0]);
  const [guessIndex, setGuessIndex] = React.useState(0);
  const [guesses, setGuesses] = React.useState < IGuess > defaultGuess;
  const [gameComplete, setGameComplete] = React.useState(false);

  const handleKeyPress = (letter: string) => {
    const guess: string = guesses[guessIndex];

    if (letter === 'ENTER') {
      if (guess.length !== 5) {
        alert('Word too short.');
        return;
      }

      if (!words.includes(guess)) {
        alert('Not a valid word.');
        return;
      }

      if (guess === activeWord) {
        setGuessIndex(guessIndex + 1);
        setGameComplete(true);
        alert('You win!');
        return;
      }

      if (guessIndex < 5) {
        setGuessIndex(guessIndex + 1);
      } else {
        setGameComplete(true);
        alert('You lose!');
        return;
      }
    }

    if (letter === '') {
      setGuesses({ ...guesses, [guessIndex]: guess.slice(0, -1) });
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuesses({ ...guesses, [guessIndex]: guess + letter });
  };

  React.useEffect(() => {
    if (!gameComplete) {
      setActiveWord(words[Math.floor(Math.random() * words.length)]);
      setGuesses(defaultGuess);
      setGuessIndex(0);
    }
  }, [gameComplete]);

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow
          guess={guesses[0]}
          word={activeWord}
          guessed={guessIndex > 0}
        />
        <GuessRow
          guess={guesses[1]}
          word={activeWord}
          guessed={guessIndex > 1}
        />
        <GuessRow
          guess={guesses[2]}
          word={activeWord}
          guessed={guessIndex > 2}
        />
        <GuessRow
          guess={guesses[3]}
          word={activeWord}
          guessed={guessIndex > 3}
        />
        <GuessRow
          guess={guesses[4]}
          word={activeWord}
          guessed={guessIndex > 4}
        />
        <GuessRow
          guess={guesses[5]}
          word={activeWord}
          guessed={guessIndex > 5}
        />
      </View>
      <View>
        {gameComplete ? (
          <View style={styles.gameCompleteWrapper}>
            <Text>
              <Text style={styles.bold}>Correct Word:</Text> {activeWord}
            </Text>
            <View>
              <Button
                title="Reset"
                onPress={() => {
                  setGameComplete(false);
                }}
              />
            </View>
          </View>
        ) : null}
        <Keyboard onKeyPress={handleKeyPress} />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  // ...
  keyLetter: {
    fontWeight: '500',
    fontSize: 15,
  },

  // Game complete
  gameCompleteWrapper: {
    alignItems: 'center',
  },
  bold: {
    fontWeight: 'bold',
  },
});
Enter fullscreen mode Exit fullscreen mode

That’s all we’ll cover today. You’ve got a pretty functional version of Wordle, in one file, using React Native.

Additional features to add that ours is missing include:

  • Indicate if a letter is correctly placed, in the word, or not in the word on the keyboard
  • Create a shareable emoji to show the user’s guess journey when the game is complete.

Final Code

The final file for our React Native Wordle clone is:

import React from 'react';
import {
  StyleSheet,
  View,
  SafeAreaView,
  Text,
  TouchableOpacity,
  Button,
} from 'react-native';

const Block = ({
  index,
  guess,
  word,
  guessed,
}: {
  index: number,
  guess: string,
  word: string,
  guessed: boolean,
}) => {
  const letter = guess[index];
  const wordLetter = word[index];

  const blockStyles: any[] = [styles.guessSquare];
  const textStyles: any[] = [styles.guessLetter];

  if (letter === wordLetter && guessed) {
    blockStyles.push(styles.guessCorrect);
    textStyles.push(styles.guessedLetter);
  } else if (word.includes(letter) && guessed) {
    blockStyles.push(styles.guessInWord);
    textStyles.push(styles.guessedLetter);
  } else if (guessed) {
    blockStyles.push(styles.guessNotInWord);
    textStyles.push(styles.guessedLetter);
  }

  return (
    <View style={blockStyles}>
      <Text style={textStyles}>{letter}</Text>
    </View>
  );
};

const GuessRow = ({
  guess,
  word,
  guessed,
}: {
  guess: string,
  word: string,
  guessed: boolean,
}) => {
  return (
    <View style={styles.guessRow}>
      <Block index={0} guess={guess} word={word} guessed={guessed} />
      <Block index={1} guess={guess} word={word} guessed={guessed} />
      <Block index={2} guess={guess} word={word} guessed={guessed} />
      <Block index={3} guess={guess} word={word} guessed={guessed} />
      <Block index={4} guess={guess} word={word} guessed={guessed} />
    </View>
  );
};

const KeyboardRow = ({
  letters,
  onKeyPress,
}: {
  letters: string[],
  onKeyPress: (letter: string) => void,
}) => (
  <View style={styles.keyboardRow}>
    {letters.map((letter) => (
      <TouchableOpacity onPress={() => onKeyPress(letter)} key={letter}>
        <View style={styles.key}>
          <Text style={styles.keyLetter}>{letter}</Text>
        </View>
      </TouchableOpacity>
    ))}
  </View>
);

const Keyboard = ({ onKeyPress }: { onKeyPress: (letter: string) => void }) => {
  const row1 = ['Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P'];
  const row2 = ['A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L'];
  const row3 = ['Z', 'X', 'C', 'V', 'B', 'N', 'M', ''];

  return (
    <View style={styles.keyboard}>
      <KeyboardRow letters={row1} onKeyPress={onKeyPress} />
      <KeyboardRow letters={row2} onKeyPress={onKeyPress} />
      <KeyboardRow letters={row3} onKeyPress={onKeyPress} />
      <View style={styles.keyboardRow}>
        <TouchableOpacity onPress={() => onKeyPress('ENTER')}>
          <View style={styles.key}>
            <Text style={styles.keyLetter}>ENTER</Text>
          </View>
        </TouchableOpacity>
      </View>
    </View>
  );
};

const words = [
  'LIGHT',
  'TIGHT',
  'GOING',
  'WRUNG',
  'COULD',
  'PERKY',
  'MOUNT',
  'WHACK',
  'SUGAR',
];

interface IGuess {
  [key: number]: string;
}

const defaultGuess: IGuess = {
  0: '',
  1: '',
  2: '',
  3: '',
  4: '',
  5: '',
};

export default function App() {
  const [activeWord, setActiveWord] = React.useState(words[0]);
  const [guessIndex, setGuessIndex] = React.useState(0);
  const [guesses, setGuesses] = React.useState < IGuess > defaultGuess;
  const [gameComplete, setGameComplete] = React.useState(false);

  const handleKeyPress = (letter: string) => {
    const guess: string = guesses[guessIndex];

    if (letter === 'ENTER') {
      if (guess.length !== 5) {
        alert('Word too short.');
        return;
      }

      if (!words.includes(guess)) {
        alert('Not a valid word.');
        return;
      }

      if (guess === activeWord) {
        setGuessIndex(guessIndex + 1);
        setGameComplete(true);
        alert('You win!');
        return;
      }

      if (guessIndex < 5) {
        setGuessIndex(guessIndex + 1);
      } else {
        setGameComplete(true);
        alert('You lose!');
        return;
      }
    }

    if (letter === '') {
      setGuesses({ ...guesses, [guessIndex]: guess.slice(0, -1) });
      return;
    }

    // don't add if guess is full
    if (guess.length >= 5) {
      return;
    }

    setGuesses({ ...guesses, [guessIndex]: guess + letter });
  };

  React.useEffect(() => {
    if (!gameComplete) {
      setActiveWord(words[Math.floor(Math.random() * words.length)]);
      setGuesses(defaultGuess);
      setGuessIndex(0);
    }
  }, [gameComplete]);

  return (
    <SafeAreaView style={styles.container}>
      <View>
        <GuessRow
          guess={guesses[0]}
          word={activeWord}
          guessed={guessIndex > 0}
        />
        <GuessRow
          guess={guesses[1]}
          word={activeWord}
          guessed={guessIndex > 1}
        />
        <GuessRow
          guess={guesses[2]}
          word={activeWord}
          guessed={guessIndex > 2}
        />
        <GuessRow
          guess={guesses[3]}
          word={activeWord}
          guessed={guessIndex > 3}
        />
        <GuessRow
          guess={guesses[4]}
          word={activeWord}
          guessed={guessIndex > 4}
        />
        <GuessRow
          guess={guesses[5]}
          word={activeWord}
          guessed={guessIndex > 5}
        />
      </View>
      <View>
        {gameComplete ? (
          <View style={styles.gameCompleteWrapper}>
            <Text>
              <Text style={styles.bold}>Correct Word:</Text> {activeWord}
            </Text>
            <View>
              <Button
                title="Reset"
                onPress={() => {
                  setGameComplete(false);
                }}
              />
            </View>
          </View>
        ) : null}
        <Keyboard onKeyPress={handleKeyPress} />
      </View>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  guessRow: {
    flexDirection: 'row',
    justifyContent: 'center',
  },
  guessSquare: {
    borderColor: '#d3d6da',
    borderWidth: 2,
    width: 50,
    height: 50,
    alignItems: 'center',
    justifyContent: 'center',
    margin: 5,
  },
  guessLetter: {
    fontSize: 20,
    fontWeight: 'bold',
    color: '#878a8c',
  },
  guessedLetter: {
    color: '#fff',
  },
  guessCorrect: {
    backgroundColor: '#6aaa64',
    borderColor: '#6aaa64',
  },
  guessInWord: {
    backgroundColor: '#c9b458',
    borderColor: '#c9b458',
  },
  guessNotInWord: {
    backgroundColor: '#787c7e',
    borderColor: '#787c7e',
  },

  container: {
    justifyContent: 'space-between',
    flex: 1,
  },

  // keyboard
  keyboard: { flexDirection: 'column' },
  keyboardRow: {
    flexDirection: 'row',
    justifyContent: 'center',
    marginBottom: 10,
  },
  key: {
    backgroundColor: '#d3d6da',
    padding: 10,
    margin: 3,
    borderRadius: 5,
  },
  keyLetter: {
    fontWeight: '500',
    fontSize: 15,
  },

  // Game complete
  gameCompleteWrapper: {
    alignItems: 'center',
  },
  bold: {
    fontWeight: 'bold',
  },
});
Enter fullscreen mode Exit fullscreen mode

Top comments (2)

Collapse
 
lissy93 profile image
Alicia Sykes

Really excelantly written post Spencer, each step clearly explained :)

Collapse
 
ubaidseo001 profile image
ubaidseo001

thats amazing tried the same code on wordle-unlimited.us/