Build a React JS Interactive Simple Interactive Component: A Basic Interactive Game of Tic-Tac-Toe

Ever wanted to build your own game? Let’s dive into creating a classic: Tic-Tac-Toe, using React JS. This tutorial is designed for beginners and intermediate developers, guiding you through each step. We’ll break down the concepts, provide code examples, and discuss common pitfalls. By the end, you’ll have a fully functional Tic-Tac-Toe game and a solid understanding of React’s core principles.

Why Build Tic-Tac-Toe with React?

Tic-Tac-Toe is an excellent project for learning React. It allows you to grasp fundamental concepts like:

  • Components: Building reusable UI elements.
  • State: Managing dynamic data within your application.
  • Props: Passing data between components.
  • Event Handling: Responding to user interactions.
  • Conditional Rendering: Displaying different content based on conditions.

Moreover, building a game is fun! It provides immediate feedback and a clear goal, making the learning process engaging. Plus, the skills you learn are transferable to more complex React applications.

Setting Up Your React Project

Before we start, you’ll need Node.js and npm (Node Package Manager) or yarn installed on your machine. These are essential for managing project dependencies. If you don’t have them, download and install them from the official Node.js website.

Next, let’s create a new React project using Create React App. Open your terminal or command prompt and run the following command:

npx create-react-app tic-tac-toe-game
cd tic-tac-toe-game

This command creates a new React project named “tic-tac-toe-game”. The `cd` command navigates into the project directory. Now, you can start the development server by running:

npm start

This command starts the development server, and your Tic-Tac-Toe game will open in your web browser (usually at `http://localhost:3000`).

Building the Tic-Tac-Toe Board Component

Let’s start by creating the building block of our game: the board. The board will consist of nine squares, each representing a cell in the Tic-Tac-Toe grid. We’ll create a `Square` component and a `Board` component to manage these squares.

Creating the Square Component

Create a file named `Square.js` inside the `src` directory. This component will render a single square on the board. Here’s the code:

import React from 'react';

function Square(props) {
  return (
    <button>
      {props.value}
    </button>
  );
}

export default Square;

Let’s break down the `Square` component:

  • `import React from ‘react’;`: Imports the React library.
  • `function Square(props)`: Defines the `Square` component as a function. Function components are a common and effective way to define components in React.
  • `props`: This object contains data passed to the component from its parent component (in this case, the `Board` component).
  • `onClick={props.onClick}`: This sets the `onClick` event handler for the button. When the button is clicked, it will call the function passed through the `onClick` prop.
  • `{props.value}`: This displays the value of the square (X, O, or null) passed through the `value` prop.
  • `

Creating the Board Component

Now, create a file named `Board.js` inside the `src` directory. The `Board` component will render the nine `Square` components.

import React, { useState } from 'react';
import Square from './Square';

function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));
  const [xIsNext, setXIsNext] = useState(true);

  const handleClick = (i) => {
    const newSquares = [...squares];
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    newSquares[i] = xIsNext ? 'X' : 'O';
    setSquares(newSquares);
    setXIsNext(!xIsNext);
  };

  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
  }

  function renderSquare(i) {
    return (
       handleClick(i)}
      />
    );
  }

  return (
    <div>
      <div>{status}</div>
      <div>
        {renderSquare(0)}{renderSquare(1)}{renderSquare(2)}
      </div>
      <div>
        {renderSquare(3)}{renderSquare(4)}{renderSquare(5)}
      </div>
      <div>
        {renderSquare(6)}{renderSquare(7)}{renderSquare(8)}
      </div>
    </div>
  );
}

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

export default Board;

Let’s break down the `Board` component:

  • `import React, { useState } from ‘react’;`: Imports React and the `useState` hook. The `useState` hook allows us to manage state within the component.
  • `import Square from ‘./Square’;`: Imports the `Square` component.
  • `const [squares, setSquares] = useState(Array(9).fill(null));`: This line uses the `useState` hook to initialize the `squares` state. `squares` is an array of 9 elements, initially filled with `null`. `setSquares` is a function that allows us to update the `squares` state.
  • `const [xIsNext, setXIsNext] = useState(true);`: Another `useState` hook, this time for tracking whose turn it is. `xIsNext` is a boolean, initially `true` (X goes first). `setXIsNext` updates the value.
  • `handleClick(i)`: This function is called when a square is clicked. It takes the index `i` of the clicked square as an argument. Inside the function, the following actions take place:
    • Creates a copy of the `squares` array using the spread operator (`…`). This is crucial to avoid directly modifying the state, which is a common React best practice.
    • Checks if there’s a winner or if the square is already filled. If either is true, it returns, preventing further moves on the same square.
    • Updates the `newSquares` array with either ‘X’ or ‘O’, depending on `xIsNext`.
    • Calls `setSquares(newSquares)` to update the state, triggering a re-render of the `Board` component.
    • Calls `setXIsNext(!xIsNext)` to switch to the other player’s turn.
  • `calculateWinner(squares)`: This function, defined later in the code, determines if there’s a winner based on the current state of the `squares` array.
  • `renderSquare(i)`: This function renders a single `Square` component. It passes the current value of the square (`squares[i]`) and a function (`handleClick(i)`) to the `Square` component as props.
  • The `return` statement: This is where the board is rendered. It displays the status (who’s turn it is or who won) and the nine squares arranged in three rows.
  • `calculateWinner` Function: This function checks all possible winning combinations to determine the winner. It takes the `squares` array as input and returns ‘X’, ‘O’, or `null` if there’s no winner.

Integrating the Board into App.js

Now, let’s integrate the `Board` component into our main application. Open `src/App.js` and modify it as follows:

import React from 'react';
import Board from './Board';
import './App.css'; // Import the CSS file

function App() {
  return (
    <div>
      <div>
        
      </div>
      <div>
        {/* You'll add game information here later */} 
      </div>
    </div>
  );
}

export default App;

And create `src/App.css` with the following content (or your own styling):

.game {
  display: flex;
  flex-direction: row;
}

.game-board {
  margin-right: 20px;
}

.square {
  background: #fff;
  border: 1px solid #999;
  float: left;
  font-size: 24px;
  font-weight: bold;
  line-height: 34px;
  height: 34px;
  margin-right: -1px;
  margin-top: -1px;
  padding: 0;
  text-align: center;
  width: 34px;
}

.square:focus {
  outline: none;
}

.kbd-navigation .square:focus {
  background: #ddd;
}

.game-info {
  margin-left: 20px;
}

.board-row:after {
  clear: both;
  content: "";
  display: table;
}

.status {
  margin-bottom: 10px;
}

This code imports the `Board` component and renders it within a `div` with the class “game”. The CSS provides basic styling for the game board and squares.

Adding Functionality: Handling Clicks and Updating the Board

The `handleClick` function in the `Board` component is the heart of the game’s logic. Let’s revisit it and understand how it works.

  const handleClick = (i) => {
    const newSquares = [...squares];
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    newSquares[i] = xIsNext ? 'X' : 'O';
    setSquares(newSquares);
    setXIsNext(!xIsNext);
  };

Here’s a breakdown:

  • `const newSquares = […squares];`: Creates a copy of the `squares` array using the spread syntax. This is crucial to avoid directly modifying the original state, which is a fundamental principle in React. Directly modifying the state can lead to unexpected behavior and make it difficult to debug your application.
  • `if (calculateWinner(squares) || squares[i]) { return; }`: This line checks if there is a winner already or if the clicked square is already filled. If either condition is true, the function returns early, preventing further moves.
  • `newSquares[i] = xIsNext ? ‘X’ : ‘O’;`: This line updates the `newSquares` array with either ‘X’ or ‘O’, depending on whose turn it is. The ternary operator (`xIsNext ? ‘X’ : ‘O’`) concisely determines the player’s mark.
  • `setSquares(newSquares);`: This line updates the `squares` state with the modified `newSquares` array. This triggers a re-render of the `Board` component, displaying the updated board.
  • `setXIsNext(!xIsNext);`: This line toggles the `xIsNext` state, switching to the other player’s turn.

Determining the Winner

The `calculateWinner` function is responsible for determining the winner of the game. Let’s examine its code again:

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

Here’s a breakdown:

  • `const lines = […]`: This array defines all the winning combinations in Tic-Tac-Toe. Each inner array represents a row, column, or diagonal.
  • `for (let i = 0; i < lines.length; i++) { … }`: This loop iterates through each winning combination.
  • `const [a, b, c] = lines[i];`: This line destructures the current winning combination into three variables, `a`, `b`, and `c`, representing the indices of the squares.
  • `if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) { return squares[a]; }`: This is the core logic. It checks if the squares at indices `a`, `b`, and `c` are all filled with the same value (either ‘X’ or ‘O’). If they are, it means a player has won, and the function returns the winning player’s mark (‘X’ or ‘O’).
  • `return null;`: If the loop completes without finding a winner, the function returns `null`, indicating that there is no winner yet.

Adding Game Status and Reset Functionality

Let’s enhance our game by displaying the game status (who’s turn it is or who won) and adding a reset button to start a new game.

Displaying the Game Status

We’ve already implemented the status display within the `Board` component. The `status` variable updates based on whether there’s a winner or whose turn it is.

  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
  }

This code snippet determines the game status based on the `winner` variable. It displays “Winner: X” or “Winner: O” if there is a winner, or “Next player: X” or “Next player: O” if the game is still in progress. The `status` is then rendered in the `return` statement.

Adding a Reset Button

To add a reset button, we’ll need to create a new function in the `Board` component that resets the game state. Modify your `Board.js` file as follows:

import React, { useState } from 'react';
import Square from './Square';

function Board() {
  const [squares, setSquares] = useState(Array(9).fill(null));
  const [xIsNext, setXIsNext] = useState(true);

  const handleClick = (i) => {
    const newSquares = [...squares];
    if (calculateWinner(squares) || squares[i]) {
      return;
    }
    newSquares[i] = xIsNext ? 'X' : 'O';
    setSquares(newSquares);
    setXIsNext(!xIsNext);
  };

  const winner = calculateWinner(squares);
  let status;
  if (winner) {
    status = 'Winner: ' + winner;
  } else {
    status = 'Next player: ' + (xIsNext ? 'X' : 'O');
  }

  const resetGame = () => {
    setSquares(Array(9).fill(null));
    setXIsNext(true);
  };

  function renderSquare(i) {
    return (
       handleClick(i)}
      />
    );
  }

  return (
    <div>
      <div>{status}</div>
      <div>
        {renderSquare(0)}{renderSquare(1)}{renderSquare(2)}
      </div>
      <div>
        {renderSquare(3)}{renderSquare(4)}{renderSquare(5)}
      </div>
      <div>
        {renderSquare(6)}{renderSquare(7)}{renderSquare(8)}
      </div>
      <button>Reset Game</button>
    </div>
  );
}

function calculateWinner(squares) {
  const lines = [
    [0, 1, 2],
    [3, 4, 5],
    [6, 7, 8],
    [0, 3, 6],
    [1, 4, 7],
    [2, 5, 8],
    [0, 4, 8],
    [2, 4, 6],
  ];
  for (let i = 0; i < lines.length; i++) {
    const [a, b, c] = lines[i];
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

export default Board;

Here’s what changed:

  • `const resetGame = () => { … }`: This function resets the game state. It sets the `squares` array back to its initial state (an array of 9 `null` values) and sets `xIsNext` to `true`, so X starts the new game.
  • ``: A button is added to the `Board` component. When clicked, it calls the `resetGame` function.

Common Mistakes and Troubleshooting

Here are some common mistakes and how to fix them when building a React Tic-Tac-Toe game:

  • Incorrect State Updates: One of the most common mistakes is directly modifying the state instead of using the `setSquares` or `setXIsNext` functions. This can lead to the UI not updating correctly. Always create a copy of the state array (using the spread syntax: `…squares`) before modifying it.
  • Forgetting to Import Components: Make sure you import all the necessary components (like `Square`) into the component files where you’re using them.
  • Incorrect Prop Passing: Double-check that you’re passing the correct props to your components. For example, ensure you’re passing the `value` and `onClick` props to the `Square` component.
  • CSS Issues: If your game isn’t styled correctly, review your CSS (or the CSS provided in this tutorial) and make sure you’ve applied the correct class names. Also, check for any CSS conflicts.
  • Infinite Loops: Be careful with event handlers and state updates. Ensure your event handlers don’t trigger infinite loops by accidentally updating the state repeatedly, causing the component to re-render indefinitely.
  • Not Using the Correct `this` Context (in older class-based components): While this tutorial uses functional components and hooks, if you encounter older code using class components, ensure you bind the `this` context correctly in event handlers (e.g., using `this.handleClick = this.handleClick.bind(this)` in the constructor).

Key Takeaways and Best Practices

Let’s summarize the key takeaways from this tutorial and some best practices for building React applications:

  • Component-Based Architecture: React applications are built using components, which are reusable UI elements.
  • State Management: Use the `useState` hook to manage the state of your components. The state represents the data that can change over time.
  • Props for Data Passing: Use props (short for properties) to pass data from parent components to child components.
  • Event Handling: Use event handlers (like `onClick`) to respond to user interactions.
  • Immutability: Always treat state as immutable. When updating state, create a copy of the existing state and modify the copy. Then, use the state update function (e.g., `setSquares`) to update the state. This ensures that React can efficiently detect changes and re-render the UI.
  • Conditional Rendering: Use conditional rendering (e.g., using the ternary operator or `if/else` statements) to display different content based on the state of your application.
  • Keep Components Focused: Each component should have a specific responsibility and be as simple as possible.
  • Use CSS for Styling: Use CSS to style your components. You can use external CSS files, inline styles, or CSS-in-JS solutions.
  • Testing: Write tests to ensure your components work as expected.
  • Code Formatting: Use a consistent code style (e.g., using a code formatter like Prettier) to improve readability and maintainability.

FAQ

Here are some frequently asked questions about building a Tic-Tac-Toe game with React:

  1. How can I add a draw condition? You can add a draw condition by checking if all the squares are filled and there is no winner. Modify the `handleClick` function to check if the game is a draw.
  2. How can I add a history feature (undo)? To add a history feature, you can store the history of moves in an array. Each element in the array would represent the state of the board after a move. You would also need to add “Back” and “Next” buttons to navigate through the history.
  3. How can I make the game more visually appealing? You can improve the visual appeal by using CSS to style the board, squares, and game information. You can also add animations and transitions. Consider using an existing UI library like Material UI or Bootstrap to speed up the styling process.
  4. How can I deploy my game? You can deploy your game using services like Netlify, Vercel, or GitHub Pages. These services allow you to easily deploy static websites, including React applications.

This tutorial has walked you through creating a basic Tic-Tac-Toe game in React. By understanding the concepts and following the steps, you’ve gained practical experience with React’s core principles. From here, you can expand upon this foundation to build more complex applications.