Building a Tic Tac Toe Game in React Native

Introduction

In this tutorial, we'll build a simple Tic Tac Toe game using React Native. This project will help you understand the basics of React Native, including components, state management, and event handling.

Prerequisites

  • Basic knowledge of JavaScript and React.

  • Node.js and npm installed on your machine.

  • React Native environment setup. You can follow the official React Native guide for setting up your environment.

Step 1: Setting Up the Project

First, create a new React Native project using the following command:

npx react-native init Tic-Tac-Toe

Navigate into the project directory:

cd Tic-Tac-Toe

Step 2: react-native-vector-icons

Installation

  1. Install the package via npm:

     npm install --save react-native-vector-icons
    
  2. Depending on the platform you're targeting (iOS/Android/Windows), follow the appropriate setup instructions.

  3. If you're planning to useFontAwesome 5or6 icons, refer to these guides:FontAwesome 5|FontAwesome 6

iOS Setup

To use the bundled icons on iOS, follow these steps:

  • Navigate to node_modules/react-native-vector-icons and drag the Fonts folder (or select specific fonts) into your Xcode project.

  • Edit Info.plist and add a property called Fonts providedby application (or UIAppFonts if Xcode autocomplete is not working):

  • List of all available fonts to copy & paste in Info.plist
    <key>UIAppFonts</key> <array> <string>AntDesign.ttf</string> <string>Entypo.ttf</string> <string>EvilIcons.ttf</string> <string>Feather.ttf</string> <string>FontAwesome.ttf</string> <string>FontAwesome5_Brands.ttf</string> <string>FontAwesome5_Regular.ttf</string> <string>FontAwesome5_Solid.ttf</string> <string>FontAwesome6_Brands.ttf</string> <string>FontAwesome6_Regular.ttf</string> <string>FontAwesome6_Solid.ttf</string> <string>Foundation.ttf</string> <string>Ionicons.ttf</string> <string>MaterialIcons.ttf</string> <string>MaterialCommunityIcons.ttf</string> <string>SimpleLineIcons.ttf</string> <string>Octicons.ttf</string> <string>Zocial.ttf</string> <string>Fontisto.ttf</string> </array>

    Above step might look something Like this:

  • n Xcode, select your project in the navigator, choose your app's target, go to the Build Phases tab, and under Copy Bundle Resources, add the copied fonts.

  • When using auto linking, it will automatically add all fonts to the Build Phases, Copy Pods Resources. Which will end up in your bundle. To avoid that, create a react-native.config.js file at the root of your react-native project with:

        module.exports = {
          dependencies: {
            'react-native-vector-icons': {
              platforms: {
                ios: null,
              },
            },
          },
        };
    

Note: Recompile your project after adding new fonts.

Note 2: If you're getting problems withduplicate outputs file for fonts on ios build, try running cd ios && pod install after the react-native.config.js configuration.

Android Setup

To make font management smoother on Android, use this method:

  • Edit android/app/build.gradle (NOT android/build.gradle) and add:

      apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")
    

    To customize the fonts being copied, use:

      project.ext.vectoricons = [
          iconFontNames: [ 'MaterialIcons.ttf', 'EvilIcons.ttf' ] // Specify font files
      ]
    
      apply from: file("../../node_modules/react-native-vector-icons/fonts.gradle")
    

Option: Manual Integration

To manually integrate the library, follow these steps:

  • Copy the contents from the Fonts folder and paste them into android/app/src/main/assets/fonts (ensure the folder name is lowercase, i.e., fonts).

Integrating Library forgetImageSource Support

The following steps are optional and are only necessary if you intend to utilize the Icon.getImageSource function.

  • Edit the android/settings.gradle file as shown below:

      rootProject.name = 'MyApp'
    
      include ':app'
    
      + include ':react-native-vector-icons'
      + project(':react-native-vector-icons').projectDir = new File(rootProject.projectDir, '../node_modules/react-native-vector-icons/android')
    
  • Edit the android/app/build.gradle (located in the app folder) as shown below:

      apply plugin: 'com.android.application'
    
      android {
        ...
      }
    
      dependencies {
        implementation fileTree(dir: "libs", include: ["*.jar"])
        //noinspection GradleDynamicVersion
        implementation "com.facebook.react:react-native:+"  // From node_modules
    
      + implementation project(':react-native-vector-icons')
      }
    
  • Edit your MainApplication.java (located deep within android/app/src/main/java/...) as shown below (note that there are two places to edit):

      package com.myapp;
    
      + import com.oblador.vectoricons.VectorIconsPackage;
    
      ....
    
        @Override
        protected List<ReactPackage> getPackages() {
          return Arrays.<ReactPackage>asList(
            new MainReactPackage()
      +   , new VectorIconsPackage()
          );
        }
    

Please note that this optional step is necessary only if your react-native app doesn't support auto-linking; otherwise, you can skip this.

Step 3: Install @types/react-navigate-vector-Icon:

Installation

npm install --save @types/react-native-vector-icons

Summary

This package contains type definitions for react-native-vector-icons (github.com/oblador/react-native-vector-icons).

Step 4 : Creating the Game Board

Create a new component for the game board. Create a file named App.js in the src directory

Inside src directory create a Component folder and make Icon.tsx

App.tsx code:

import React, { useState } from 'react';
import {
  FlatList,
  Pressable,
  SafeAreaView,
  StatusBar,
  StyleSheet,
  Text,
  View,
} from 'react-native';
import Snackbar from 'react-native-snackbar';
import Icons from './components/Icons';

function App(): JSX.Element {
  const [isCross, setIsCross] = useState<boolean>(false);
  const [gameWinner, setGameWinner] = useState<string>('');
  const [gameState, setGameState] = useState<string[]>(new Array(9).fill('empty'));

  const reloadGame = () => {
    setIsCross(false);
    setGameWinner('');
    setGameState(new Array(9).fill('empty'));
  };

  const checkIsWinner = (state: string[]) => {
    const winningPatterns = [
      [0, 1, 2], [3, 4, 5], [6, 7, 8], // rows
      [0, 3, 6], [1, 4, 7], [2, 5, 8], // columns
      [0, 4, 8], [2, 4, 6]             // diagonals
    ];

    for (let pattern of winningPatterns) {
      const [a, b, c] = pattern;
      if (state[a] !== 'empty' && state[a] === state[b] && state[a] === state[c]) {
        setGameWinner(`${state[a]} won the game! 🥳`);
        return true;
      }
    }

    if (!state.includes('empty')) {
      setGameWinner('Draw game... ⌛️');
      return true;
    }

    return false;
  };

  const onChangeItem = (itemNumber: number) => {
    if (gameWinner) {
      Snackbar.show({
        text: gameWinner,
        backgroundColor: '#FFFFFF',
        textColor: "#FFFFFF"
      });
      return;
    }

    if (gameState[itemNumber] === 'empty') {
      const newGameState = [...gameState];
      newGameState[itemNumber] = isCross ? 'cross' : 'circle';
      setGameState(newGameState);

      if (!checkIsWinner(newGameState)) {
        setIsCross(!isCross);
      }
    } else {
      Snackbar.show({
        text: "Position is already filled",
        backgroundColor: "red",
        textColor: "#FFF"
      });
    }
  };

  return (
    <SafeAreaView style={styles.container}>
      <StatusBar />
      {gameWinner ? (
        <View style={[styles.playerInfo, styles.winnerInfo]}>
          <Text style={styles.winnerTxt}>{gameWinner}</Text>
        </View>
      ) : (
        <View style={[styles.playerInfo, isCross ? styles.playerX : styles.playerO]}>
          <Text style={styles.gameTurnTxt}>
            Player {isCross ? 'X' : 'O'}'s Turn
          </Text>
        </View>
      )}
      <FlatList
        numColumns={3}
        data={gameState}
        style={styles.grid}
        renderItem={({ item, index }) => (
          <Pressable
            key={index}
            style={styles.card}
            onPress={() => onChangeItem(index)}
          >
            <Icons name={item} />
          </Pressable>
        )}
        keyExtractor={(item, index) => index.toString()}
      />
      <Pressable style={styles.gameBtn} onPress={reloadGame}>
        <Text style={styles.gameBtnText}>
          {gameWinner ? 'Start new game' : 'Reload the game'}
        </Text>
      </Pressable>
    </SafeAreaView>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    justifyContent: 'center',
    paddingHorizontal: 16,
  },
  playerInfo: {
    height: 56,
    flexDirection: 'row',
    justifyContent: 'center',
    alignItems: 'center',
    borderRadius: 4,
    paddingVertical: 8,
    marginVertical: 12,
    marginHorizontal: 14,
    shadowOffset: { width: 1, height: 1 },
    shadowColor: '#333',
    shadowOpacity: 0.2,
    shadowRadius: 1.5,
  },
  gameTurnTxt: {
    fontSize: 20,
    color: '#FFFFFF',
    fontWeight: '600',
  },
  playerX: {
    backgroundColor: '#38CC77',
  },
  playerO: {
    backgroundColor: '#F7CD2E',
  },
  grid: {
    margin: 12,
  },
  card: {
    height: 100,
    width: '33.33%',
    alignItems: 'center',
    justifyContent: 'center',
    borderWidth: 1,
    borderColor: '#FFF',
  },
  winnerInfo: {
    borderRadius: 8,
    backgroundColor: '#38CC77',
    shadowOpacity: 0.1,
  },
  winnerTxt: {
    fontSize: 20,
    color: '#FFFFFF',
    fontWeight: '600',
    textTransform: 'capitalize',
  },
  gameBtn: {
    alignItems: 'center',
    padding: 10,
    borderRadius: 8,
    marginHorizontal: 36,
    backgroundColor: '#8D3DAF',
    marginBottom: 450,
  },
  gameBtnText: {
    fontSize: 18,
    color: '#FFFFFF',
    fontWeight: '500',
  },
});

export default App;

Step 3: Using the Board Component

Now, use the src component in your main Icon.tsx file.

Icon.tsx code:

import React from 'react'
import { PropsWithChildren } from 'react'
import Icon from 'react-native-vector-icons/FontAwesome'
type IconsProps=PropsWithChildren<{
    name: string;
}>
const Icons=({name}:IconsProps)=> {
    switch (name) {
        case 'circle':
            return <Icon name="circle-thin" size={38} color="#000000"/>
            break;
        case 'cross':
            return <Icon name="times" size={38} color="#FFFFFF"/>
            break;
        default:
            return <Icon name="pincle" size={38} color="#FFFFFF"/>

    }
}

export default Icons

Step 4: Running the App

Run your app on an emulator or a physical device using the following command:

npx react-native run-android

or

npx react-native run-ios

Conclusion

Congratulations! You've built a simple Tic Tac Toe game using React Native. This tutorial covered the basics of React Native components, state management, and event handling. You can further enhance this game by adding more features, such as a score tracker, different difficulty levels, or even a multiplayer mode.

For more advanced React Native tutorials and resources, check out the official React Native documentation.