Learn and Teach Coding

Tic Tac Toe Game

A complete Tic Tac Toe game in React with win detection and a reset button.

ReactIntermediatestategameuseState

Live preview

Player X's turn

Source code

import { useState } from "react";

type Cell = "X" | "O" | null;

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],
];

function getWinner(board: Cell[]): Cell {
  for (const [a, b, c] of LINES) {
    if (board[a] && board[a] === board[b] && board[a] === board[c]) {
      return board[a];
    }
  }
  return null;
}

export default function TicTacToe() {
  const [board, setBoard] = useState<Cell[]>(Array(9).fill(null));
  const [xIsNext, setXIsNext] = useState(true);

  const winner = getWinner(board);
  const isDraw = !winner && board.every(Boolean);

  function play(i: number) {
    if (board[i] || winner) return;
    const next = board.slice();
    next[i] = xIsNext ? "X" : "O";
    setBoard(next);
    setXIsNext(!xIsNext);
  }

  function reset() {
    setBoard(Array(9).fill(null));
    setXIsNext(true);
  }

  const status = winner
    ? `Player ${winner} wins!`
    : isDraw
      ? "It's a draw!"
      : `Player ${xIsNext ? "X" : "O"}'s turn`;

  return (
    <div className="flex flex-col items-center gap-4">
      <p className="text-sm font-semibold text-slate-700 dark:text-slate-200">
        {status}
      </p>
      <div className="grid grid-cols-3 gap-2">
        {board.map((cell, i) => (
          <button
            key={i}
            type="button"
            onClick={() => play(i)}
            aria-label={`Cell ${i + 1}${cell ? `, ${cell}` : ""}`}
            className="flex h-16 w-16 items-center justify-center rounded-lg border border-slate-200 bg-slate-50 text-2xl font-bold text-slate-900 transition-colors hover:bg-slate-100 disabled:cursor-not-allowed dark:border-slate-700 dark:bg-slate-800 dark:text-white dark:hover:bg-slate-700"
          >
            <span className={cell === "X" ? "text-brand-600 dark:text-brand-400" : "text-rose-500"}>
              {cell}
            </span>
          </button>
        ))}
      </div>
      <button
        type="button"
        onClick={reset}
        className="rounded-lg bg-slate-900 px-4 py-2 text-sm font-semibold text-white transition-colors hover:bg-slate-700 dark:bg-white dark:text-slate-900 dark:hover:bg-slate-200"
      >
        Reset game
      </button>
    </div>
  );
}

Walkthrough

A classic exercise for learning React state. The whole game lives in two pieces of state, and everything else — the winner, the draw, the status text — is derived on each render. The full component is on the right.

1. The board is one array

Nine cells in a single array, plus a boolean for whose turn it is. Modelling the board as one value (rather than nine) makes updates and win-checking trivial.

type Cell = "X" | "O" | null;

const [board, setBoard] = useState<Cell[]>(Array(9).fill(null));
const [xIsNext, setXIsNext] = useState(true);

2. Winning lines

The eight possible lines (rows, columns, diagonals) are a constant outside the component — no need to rebuild them on every render.

const LINES = [
  [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
];

3. Derive, don't store

The winner and draw are computed from the board every render, so they can never drift out of sync with it.

const winner = getWinner(board);
const isDraw = !winner && board.every(Boolean);

Deriving values during render instead of storing them in state is a core React habit — less state means fewer bugs.

4. Playing a move

A click is ignored if the cell is taken or the game is over. Otherwise we copy the array (never mutate state), write the mark, and flip the turn.

function play(i: number) {
  if (board[i] || winner) return;
  const next = board.slice();
  next[i] = xIsNext ? "X" : "O";
  setBoard(next);
  setXIsNext(!xIsNext);
}

5. Status and reset

The status line is just another derived string, and Reset restores the initial state. The grid renders by mapping over board. See the full source for the markup and the getWinner helper.