Skip to main content

Tic-Tac-Toe Game Implementation - React TypeScript Frontend Design

Comprehensive low-level design for a responsive Tic-Tac-Toe game with React, TypeScript, and modern frontend architecture patterns.

This document presents a complete low-level design for implementing a Tic-Tac-Toe game with React and TypeScript, covering component structure, state management, and performance optimizations for production-ready code.

1. Problem Statement & Requirements

Clarifying questions

  1. Target devices/browsers? Will this be primarily used on mobile, desktop, or both? Should we support older browsers like IE11 or focus on modern browsers?
  2. Auth requirements? Is this a public, anonymous game or does it require user authentication to track scores or save progress?
  3. Real-time vs. static data? Should multiple players be able to play simultaneously over a network, or is this a single-player/local game?
  4. Offline support needs? Should the game work without internet connectivity and resume when reconnected?
  5. Accessibility level? What is the required accessibility compliance (WCAG AA, AAA)? Should we support screen readers and keyboard navigation?
  6. i18n requirements? Will the game need to support multiple languages or RTL layouts?
  7. Design system availability? Are we using a design system like Material UI or Tailwind, or building from scratch?

Functional requirements

  • Players can take turns placing X and O on a 3x3 grid
  • Game detects win conditions (three in a row, column, or diagonal)
  • Game detects draw conditions (all squares filled with no winner)
  • Players can restart the game at any time
  • Game displays current player's turn
  • Game shows win/draw announcement
  • Optional: Score tracking for multiple games

Non-functional requirements

  • Performance: Game should respond instantly to user interactions (LCP < 2s, INP < 200ms)
  • Accessibility: Full keyboard navigation support, ARIA roles for screen readers, proper focus management
  • Scalability: Support for multiple concurrent games if extended to multiplayer
  • Reliability: Graceful handling of edge cases (invalid moves, invalid state transitions)
  • Usability: Clear visual feedback for player actions and game state changes

Constraints and assumptions

  • Single-player mode only (no real-time multiplayer)
  • Game state persists only in browser session (no server-side storage)
  • No authentication or user accounts required
  • Mobile-first responsive design approach
  • No external dependencies beyond React and standard browser APIs
  • UI built with modern CSS (Flexbox/Grid) without CSS-in-JS

Out of scope

  • Multiplayer real-time game play (WebSocket integration)
  • User account management or persistent score tracking
  • AI opponent implementation (only basic game logic)
  • Game statistics or analytics dashboard
  • Advanced UI animations beyond basic transitions
  • Server-side rendering (SSR) or static site generation (SSG)
  • Third-party integrations (social sharing, ads, etc.)

2. Component Architecture

graph TD
    A[TicTacToeGame] --> B[GameBoard]
    A --> C[GameStatus]
    A --> D[GameControls]
    B --> E[GameSquare]
    E --> F[SquareValue]
    C --> G[CurrentPlayerIndicator]
    C --> H[GameResultMessage]
    D --> I[ResetButton]
    D --> J[UndoButton]

Component Tree Breakdown

Smart Components (Container Components):

  • TicTacToeGame - orchestrates the game flow, manages state, and handles game logic
  • GameBoard - renders the 3x3 grid and manages square interactions
  • GameControls - handles game control actions (reset, undo)

Dumb Components (Presentational Components):

  • GameSquare - renders individual squares with value and state
  • SquareValue - displays X/O or empty state
  • GameStatus - displays current player and game result
  • CurrentPlayerIndicator - visual indicator of current player
  • GameResultMessage - displays win/draw messages
  • ResetButton - UI for resetting the game
  • UndoButton - UI for undoing moves

TypeScript Props Contracts

// TicTacToeGame.tsx
interface TicTacToeGameProps {
  initialBoard?: BoardState;
  onGameEnd?: (result: GameResult) => void;
}

// GameBoard.tsx
interface GameBoardProps {
  board: BoardState;
  onSquareClick: (row: number, col: number) => void;
  isWinningSquare?: (row: number, col: number) => boolean;
}

// GameSquare.tsx
interface GameSquareProps {
  value: SquareValue;
  onClick: () => void;
  isWinning?: boolean;
  disabled?: boolean;
}

// GameStatus.tsx
interface GameStatusProps {
  currentPlayer: Player;
  gameResult: GameResult | null;
  isDraw: boolean;
}

UI State Machine

stateDiagram-v2
    [*] --> Idle
    Idle --> Playing: gameStart
    Playing --> Win: winCondition
    Playing --> Draw: drawCondition
    Win --> Idle: resetGame
    Draw --> Idle: resetGame
    Playing --> Idle: resetGame
    Playing --> Undoing: undoMove
    Undoing --> Playing: undoComplete
    
    state Playing {
        [*] --> WaitingForMove
        WaitingForMove --> Playing: moveMade
        Playing --> WaitingForMove: opponentMove
    }
    
    state Undoing {
        [*] --> UndoRequested
        UndoRequested --> UndoComplete: undoExecuted
    }

Component Responsibilities

TicTacToeGame:

  • Manages entire game state (board, current player, moves history)
  • Implements win/draw detection logic
  • Handles game reset and undo operations
  • Coordinates between child components

GameBoard:

  • Renders 3x3 grid of squares
  • Passes square interaction events to parent
  • Highlights winning squares

GameSquare:

  • Displays X/O or empty state
  • Handles click interactions
  • Applies winning square styling

GameStatus:

  • Displays current player turn
  • Shows game result (win/draw)
  • Handles status messaging

GameControls:

  • Provides reset and undo functionality
  • Manages control button states (disabled when not applicable)

Reusability Decisions

The component design prioritizes reusability through:

  1. Generic square rendering - GameSquare can be reused in other grid-based games
  2. Stateless presentation - GameStatus and GameSquare are pure presentational components
  3. Configurable props - Components accept configuration through typed props rather than hardcoded values

Component Lifecycle Considerations

  • TicTacToeGame mounts once and maintains state throughout game lifecycle
  • GameBoard re-renders on every board change but not on individual square interactions
  • GameSquare renders once per game state update with appropriate styling
  • Cleanup handled through React's automatic memory management and proper event handling

The architecture supports:

  • Easy extension to 4x4 or larger boards through props
  • Customizable game rules (win conditions, player count)
  • Themeable UI through CSS variables and design tokens
  • Responsive design for mobile/desktop experiences

3. State Management Design

Local state vs. global state decisions

For a Tic-Tac-Toe game, local state management is sufficient for most components. The core game state (board state, current player, game status) can be managed within the game component's local state. However, we'll use React Context API for sharing game configuration and theme preferences across the application.

State management approach

React Context API + Zustand for complex state

  • Primary choice: React Context API with Zustand for game state management
  • Justification:
    • Context API handles global configuration and theme preferences
    • Zustand provides lightweight, scalable state management for game logic
    • Better dev experience than Redux Toolkit for this scope
    • Easier to test and debug than complex Redux setup

State shape and normalization strategy

// Game state structure
interface GameState {
  board: (PlayerSymbol | null)[][];
  currentPlayer: PlayerSymbol;
  winner: PlayerSymbol | null;
  isDraw: boolean;
  gameStatus: 'idle' | 'playing' | 'won' | 'draw';
  moves: Move[];
  history: GameState[];
}

interface Move {
  position: [row: number, col: number];
  player: PlayerSymbol;
  timestamp: number;
}

type PlayerSymbol = 'X' | 'O';

Server state vs. client state separation

All game state is client-side only since this is a single-player or local multiplayer game. No server-side persistence is required.

Cache invalidation strategy

Since there's no server state, cache invalidation is handled by:

  • Resetting game state on new game creation
  • Clearing move history when restarting
  • Using React's re-rendering for state updates

Optimistic updates design

Optimistic UI updates:

  • Player moves are immediately reflected on the board
  • Winner detection happens after move validation
  • Game status updates instantly to prevent UI flickering

Critical-path hook implementation

// src/hooks/useTicTacToe.ts
import { useState, useCallback, useEffect } from 'react';
import { create } from 'zustand';

// Zustand store for game state
interface TicTacToeState {
  board: (PlayerSymbol | null)[][];
  currentPlayer: PlayerSymbol;
  winner: PlayerSymbol | null;
  isDraw: boolean;
  gameStatus: 'idle' | 'playing' | 'won' | 'draw';
  moves: Move[];
  history: GameState[];
  initializeGame: () => void;
  makeMove: (row: number, col: number) => boolean;
  resetGame: () => void;
  undoMove: () => void;
}

type PlayerSymbol = 'X' | 'O';

interface Move {
  position: [number, number];
  player: PlayerSymbol;
  timestamp: number;
}

interface GameState {
  board: (PlayerSymbol | null)[][];
  currentPlayer: PlayerSymbol;
  winner: PlayerSymbol | null;
  isDraw: boolean;
  gameStatus: 'idle' | 'playing' | 'won' | 'draw';
  moves: Move[];
}

const useTicTacToeStore = create<TicTacToeState>((set, get) => ({
  board: Array(3).fill(null).map(() => Array(3).fill(null)),
  currentPlayer: 'X',
  winner: null,
  isDraw: false,
  gameStatus: 'idle',
  moves: [],
  history: [],
  
  initializeGame: () => {
    set({
      board: Array(3).fill(null).map(() => Array(3).fill(null)),
      currentPlayer: 'X',
      winner: null,
      isDraw: false,
      gameStatus: 'playing',
      moves: [],
      history: [],
    });
  },

  makeMove: (row, col) => {
    const state = get();
    
    // Validate move
    if (state.board[row][col] !== null || state.gameStatus !== 'playing') {
      return false;
    }

    // Create new board
    const newBoard = state.board.map(row => [...row]);
    newBoard[row][col] = state.currentPlayer;

    // Create move record
    const newMove: Move = {
      position: [row, col],
      player: state.currentPlayer,
      timestamp: Date.now(),
    };

    // Create new game state
    const newGameState: GameState = {
      board: newBoard,
      currentPlayer: state.currentPlayer === 'X' ? 'O' : 'X',
      winner: null,
      isDraw: false,
      gameStatus: 'playing',
      moves: [...state.moves, newMove],
    };

    // Check for win or draw
    const winner = checkWinner(newBoard);
    const isDraw = !winner && newBoard.every(row => row.every(cell => cell !== null));

    if (winner) {
      newGameState.winner = winner;
      newGameState.gameStatus = 'won';
    } else if (isDraw) {
      newGameState.isDraw = true;
      newGameState.gameStatus = 'draw';
    }

    // Update state with optimistic UI
    set({
      board: newBoard,
      currentPlayer: newGameState.currentPlayer,
      winner: newGameState.winner,
      isDraw: newGameState.isDraw,
      gameStatus: newGameState.gameStatus,
      moves: newGameState.moves,
      history: [...state.history, { ...state }],
    });

    return true;
  },

  resetGame: () => {
    set({
      board: Array(3).fill(null).map(() => Array(3).fill(null)),
      currentPlayer: 'X',
      winner: null,
      isDraw: false,
      gameStatus: 'idle',
      moves: [],
      history: [],
    });
  },

  undoMove: () => {
    const state = get();
    if (state.history.length === 0) return;

    const previousState = state.history[state.history.length - 1];
    set({
      ...previousState,
      history: state.history.slice(0, -1),
    });
  },
}));

// Helper function to check for winner
function checkWinner(board: (PlayerSymbol | null)[][]): PlayerSymbol | null {
  // Check rows
  for (let i = 0; i < 3; i++) {
    if (board[i][0] && board[i][0] === board[i][1] && board[i][1] === board[i][2]) {
      return board[i][0];
    }
  }

  // Check columns
  for (let i = 0; i < 3; i++) {
    if (board[0][i] && board[0][i] === board[1][i] && board[1][i] === board[2][i]) {
      return board[0][i];
    }
  }

  // Check diagonals
  if (board[0][0] && board[0][0] === board[1][1] && board[1][1] === board[2][2]) {
    return board[0][0];
  }
  
  if (board[0][2] && board[0][2] === board[1][1] && board[1][1] === board[2][0]) {
    return board[0][2];
  }

  return null;
}

// Custom hook for Tic-Tac-Toe game logic
export const useTicTacToe = () => {
  const {
    board,
    currentPlayer,
    winner,
    isDraw,
    gameStatus,
    moves,
    initializeGame,
    makeMove,
    resetGame,
    undoMove,
  } = useTicTacToeStore();

  const handleCellClick = useCallback((row: number, col: number) => {
    return makeMove(row, col);
  }, [makeMove]);

  const handleReset = useCallback(() => {
    resetGame();
  }, [resetGame]);

  const handleUndo = useCallback(() => {
    undoMove();
  }, [undoMove]);

  return {
    board,
    currentPlayer,
    winner,
    isDraw,
    gameStatus,
    moves,
    handleCellClick,
    handleReset,
    handleUndo,
    initializeGame,
  };
};

// Return type for the hook
export interface TicTacToeHookReturn {
  board: (PlayerSymbol | null)[][];
  currentPlayer: PlayerSymbol;
  winner: PlayerSymbol | null;
  isDraw: boolean;
  gameStatus: 'idle' | 'playing' | 'won' | 'draw';
  moves: Move[];
  handleCellClick: (row: number, col: number) => boolean;
  handleReset: () => void;
  handleUndo: () => void;
  initializeGame: () => void;
}

4. Routing & Navigation

Route Structure and Hierarchy

graph TD
    A[Root] --> B[/]
    B --> C[Game]
    B --> D[Leaderboard]
    B --> E[Settings]

The application will have a single-page architecture with the following routes:

  • / - Main landing page with game selection and leaderboard
  • /game - Tic-Tac-Toe game board with player controls
  • /leaderboard - Leaderboard showing top players and scores
  • /settings - User preferences and game configuration

The root route will serve as a layout that contains the main navigation and can host the game board in a modal or inline view.

Code Splitting Strategy

Route-based code splitting with dynamic imports:

// routes.tsx
import { lazy } from 'react';

export const routes = [
  {
    path: '/',
    element: <LandingPage />,
    children: [
      {
        index: true,
        element: lazy(() => import('pages/GamePage')),
      },
      {
        path: 'leaderboard',
        element: lazy(() => import('pages/LeaderboardPage')),
      },
      {
        path: 'settings',
        element: lazy(() => import('pages/SettingsPage')),
      },
    ],
  },
];

Justification:

  • /game route will be dynamically imported to reduce initial bundle size
  • /leaderboard and /settings can be loaded on demand as they're not critical for immediate gameplay
  • This approach keeps the initial load minimal while maintaining good UX

Since this is a simple game without user accounts, navigation guards are minimal but still necessary:

  1. Public routes: All routes are accessible without authentication
  2. Game state preservation: When navigating away from game, current game state is preserved in local storage
  3. Deep link handling: URL parameters will be used to restore game state (e.g., /game?board=...&currentPlayer=X)

Deep Linking Support and URL Parameter Design

URL structure will support deep linking with:

  • ?board=0,1,2,3,4,5,6,7,8 - Board state (9 positions with 0=empty, 1=X, 2=O)
  • ?currentPlayer=X - Current player (X or O)
  • ?winner=1 - Winner state (1=X wins, 2=O wins, 0=no winner)
  • ?gameId=abc123 - Unique game identifier for multiplayer mode (future extensibility)

Browser History Management and Scroll Restoration

// App.tsx
import { BrowserRouter, useScrollRestoration } from 'react-router-dom';

function App() {
  return (
    <BrowserRouter>
      <ScrollRestoration />
      <Routes>
        {/* route definitions */}
      </Routes>
    </BrowserRouter>
  );
}

// Custom hook for scroll restoration
function useScrollRestoration() {
  useEffect(() => {
    const handleScroll = () => {
      // Save scroll position to localStorage
      localStorage.setItem('scrollPosition', window.scrollY.toString());
    };

    window.addEventListener('scroll', handleScroll);
    
    // Restore scroll position on mount
    const savedPosition = localStorage.getItem('scrollPosition');
    if (savedPosition) {
      window.scrollTo(0, parseInt(savedPosition));
    }

    return () => window.removeEventListener('scroll', handleScroll);
  }, []);

  return null;
}

The application will implement scroll restoration to maintain user context when navigating between game and leaderboard views. The scroll position will be persisted in localStorage during navigation and restored upon return to the same view.

For single-page application navigation, React Router's built-in scroll restoration will be used with custom handling for game state preservation.

5. API Integration Layer

API Client Design Pattern

For a Tic-Tac-Toe game, we'll implement a lightweight fetch wrapper that provides consistent error handling and request/response transformation. The client will be built using a custom apiClient function that wraps the native fetch API with additional utility methods.

// src/lib/apiClient.ts
export interface ApiClientConfig {
  baseUrl: string;
  timeout?: number;
}

export interface ApiRequestConfig {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE';
  headers?: Record<string, string>;
  body?: any;
}

export class ApiClient {
  private baseUrl: string;
  private timeout: number;

  constructor(config: ApiClientConfig) {
    this.baseUrl = config.baseUrl;
    this.timeout = config.timeout || 5000;
  }

  async request<T>(
    endpoint: string,
    config: ApiRequestConfig = {}
  ): Promise<T> {
    const url = `${this.baseUrl}${endpoint}`;
    const controller = new AbortController();
    const timeoutId = setTimeout(() => controller.abort(), this.timeout);

    try {
      const response = await fetch(url, {
        ...config,
        signal: controller.signal,
        headers: {
          'Content-Type': 'application/json',
          ...config.headers,
        },
      });

      clearTimeout(timeoutId);

      if (!response.ok) {
        throw new ApiError(
          `HTTP ${response.status}: ${response.statusText}`,
          response.status
        );
      }

      const data = await response.json();
      return data as T;
    } catch (error) {
      clearTimeout(timeoutId);
      if (error instanceof ApiError) throw error;
      throw new ApiError('Network error occurred', 500);
    }
  }

  get<T>(endpoint: string, config?: ApiRequestConfig): Promise<T> {
    return this.request<T>(endpoint, { ...config, method: 'GET' });
  }

  post<T>(endpoint: string, config?: ApiRequestConfig): Promise<T> {
    return this.request<T>(endpoint, { ...config, method: 'POST' });
  }

  put<T>(endpoint: string, config?: ApiRequestConfig): Promise<T> {
    return this.request<T>(endpoint, { ...config, method: 'PUT' });
  }

  delete<T>(endpoint: string, config?: ApiRequestConfig): Promise<T> {
    return this.request<T>(endpoint, { ...config, method: 'DELETE' });
  }
}

class ApiError extends Error {
  constructor(message: string, public status: number) {
    super(message);
    this.name = 'ApiError';
  }
}

Request/Response Interceptors

We'll implement a simple interceptor system that allows for authentication token injection and response normalization:

// src/lib/interceptors.ts
import { ApiClient } from './apiClient';

export interface Interceptor {
  request?(config: any): any;
  response?(response: any): any;
}

export class InterceptorManager {
  private interceptors: Interceptor[] = [];

  add(interceptor: Interceptor) {
    this.interceptors.push(interceptor);
  }

  applyRequest(config: any): any {
    let finalConfig = config;
    for (const interceptor of this.interceptors) {
      if (interceptor.request) {
        finalConfig = interceptor.request(finalConfig);
      }
    }
    return finalConfig;
  }

  applyResponse(response: any): any {
    let finalResponse = response;
    for (const interceptor of this.interceptors) {
      if (interceptor.response) {
        finalResponse = interceptor.response(finalResponse);
      }
    }
    return finalResponse;
  }
}

// Auth interceptor example
const authInterceptor: Interceptor = {
  request(config) {
    const token = localStorage.getItem('authToken');
    if (token) {
      config.headers = {
        ...config.headers,
        Authorization: `Bearer ${token}`,
      };
    }
    return config;
  },
};

// Response normalization interceptor
const responseInterceptor: Interceptor = {
  response(response) {
    // Normalize responses to a consistent structure
    return {
      data: response.data,
      status: response.status,
      timestamp: Date.now(),
    };
  },
};

// Setup interceptors
const apiClient = new ApiClient({ baseUrl: '/api' });
const interceptorManager = new InterceptorManager();

interceptorManager.add(authInterceptor);
interceptorManager.add(responseInterceptor);

// Enhanced client with interceptors
export class InterceptorApiClient extends ApiClient {
  constructor(config: ApiClientConfig) {
    super(config);
  }

  async request<T>(endpoint: string, config: ApiRequestConfig = {}): Promise<T> {
    const interceptedConfig = interceptorManager.applyRequest(config);
    return super.request<T>(endpoint, interceptedConfig);
  }
}

Error Handling and Retry Strategy

We implement a robust error handling system with retry logic for transient failures:

// src/lib/errorHandling.ts
export interface RetryConfig {
  maxRetries?: number;
  delay?: number;
  backoffMultiplier?: number;
}

export class ErrorHandler {
  static handleApiError(error: any): void {
    if (error instanceof ApiError) {
      switch (error.status) {
        case 401:
          // Redirect to login or refresh token
          localStorage.removeItem('authToken');
          window.location.href = '/login';
          break;
        case 403:
          // Handle forbidden access
          console.error('Access denied');
          break;
        case 404:
          // Handle not found
          console.error('Resource not found');
          break;
        case 500:
          // Handle server errors
          console.error('Server error occurred');
          break;
        default:
          console.error(`API Error: ${error.message}`);
      }
    } else {
      console.error('Network error:', error);
    }
  }

  static async retryWithBackoff<T>(
    fn: () => Promise<T>,
    config: RetryConfig = {}
  ): Promise<T> {
    const { maxRetries = 3, delay = 1000, backoffMultiplier = 2 } = config;
    let lastError: Error;

    for (let i = 0; i <= maxRetries; i++) {
      try {
        return await fn();
      } catch (error) {
        lastError = error as Error;
        
        if (i === maxRetries) {
          throw lastError;
        }

        // Exponential backoff
        const waitTime = delay * Math.pow(backoffMultiplier, i);
        await new Promise(resolve => setTimeout(resolve, waitTime));
      }
    }

    throw lastError!;
  }
}

Loading, Error, and Empty States Design

We'll implement a consistent state management approach for API operations:

// src/types/api.ts
export type ApiStatus = 'idle' | 'loading' | 'success' | 'error';

export interface ApiResponse<T> {
  status: ApiStatus;
  data?: T;
  error?: string;
  loading?: boolean;
}

export interface GameData {
  id: string;
  board: (string | null)[];
  currentPlayer: 'X' | 'O';
  winner?: 'X' | 'O' | 'draw';
  createdAt: string;
  updatedAt: string;
}

export interface GameMove {
  gameId: string;
  position: number;
  player: 'X' | 'O';
}

Data Fetching Strategy

For a Tic-Tac-Toe game, we'll use client-side rendering (CSR) since the game state is ephemeral and doesn't require server-side rendering:

// src/lib/gameService.ts
import { ApiClient } from './apiClient';
import { GameData, GameMove } from '../types/api';

export class GameService {
  private client: ApiClient;

  constructor(baseUrl: string) {
    this.client = new ApiClient({ baseUrl });
  }

  async createGame(): Promise<GameData> {
    try {
      const response = await this.client.post<GameData>('/games');
      return response;
    } catch (error) {
      ErrorHandler.handleApiError(error);
      throw error;
    }
  }

  async makeMove(gameId: string, move: GameMove): Promise<GameData> {
    try {
      const response = await this.client.post<GameData>(
        `/games/${gameId}/moves`,
        { body: move }
      );
      return response;
    } catch (error) {
      ErrorHandler.handleApiError(error);
      throw error;
    }
  }

  async getGame(gameId: string): Promise<GameData> {
    try {
      const response = await this.client.get<GameData>(`/games/${gameId}`);
      return response;
    } catch (error) {
      ErrorHandler.handleApiError(error);
      throw error;
    }
  }

  async getGames(): Promise<GameData[]> {
    try {
      const response = await this.client.get<GameData[]>('/games');
      return response;
    } catch (error) {
      ErrorHandler.handleApiError(error);
      throw error;
    }
  }
}

API Response Transformation Layer

We'll implement a clean DTO-to-domain model mapping:

// src/lib/transformers.ts
import { GameData } from '../types/api';

export interface GameDto {
  id: string;
  board: (string | null)[];
  current_player: 'X' | 'O';
  winner?: 'X' | 'O' | 'draw';
  created_at: string;
  updated_at: string;
}

export class GameTransformer {
  static fromDto(dto: GameDto): GameData {
    return {
      id: dto.id,
      board: dto.board,
      currentPlayer: dto.current_player,
      winner: dto.winner,
      createdAt: dto.created_at,
      updatedAt: dto.updated_at,
    };
  }

  static toDto(game: GameData): GameDto {
    return {
      id: game.id,
      board: game.board,
      current_player: game.currentPlayer,
      winner: game.winner,
      created_at: game.createdAt,
      updated_at: game.updatedAt,
    };
  }
}

Sequence Diagram for Primary Data Flows

sequenceDiagram
    participant U as User
    participant C as Client
    participant A as API Server
    participant D as Database

    U->>C: Start new game
    C->>A: POST /games
    A->>D: Create new game record
    D-->>A: Return game data
    A-->>C: GameData response
    C-->>U: Display new game board

    U->>C: Make move at position 4
    C->>A: POST /games/{id}/moves
    A->>D: Update game state
    D-->>A: Return updated game data
    A-->>C: Updated GameData
    C-->>U: Update board display

    U->>C: Check game status
    C->>A: GET /games/{id}
    A->>D: Fetch game state
    D-->>A: Return game data
    A-->>C: GameData response
    C-->>U: Show current game state

Data Flow Summary

  1. Game Creation: User initiates a new game, client makes POST request to /games endpoint
  2. Move Submission: User makes a move, client sends POST request to /games/{id}/moves with position and player
  3. Game State Retrieval: Client fetches current game state via GET /games/{id} endpoint
  4. Game Listing: Client can fetch list of games via GET /games endpoint

All API interactions follow consistent error handling patterns with proper status code interpretation and user feedback mechanisms. The design supports both local state management for immediate UI updates and server synchronization for persistent game states.

6. Performance Design

Core Web Vitals Targets

  • Largest Contentful Paint (LCP): <2.5s for game board rendering
  • Interaction to Next Paint (INP): <200ms for move interactions
  • Cumulative Layout Shift (CLS): <0.1 for all UI elements

Bundle Size Budget per Route

  • Game route: <50KB (including React, React Router, and game logic)
  • Shared components: <15KB total (reusable UI elements)
  • Lazy-loaded features: <10KB (if any additional modules)

Critical Rendering Path Optimization

  • Critical CSS: Inline minimal styles for game board and move indicators
  • Font optimization: Preload system fonts with font-display: swap
  • Code splitting: Route-based chunking for game logic and UI components
  • Minification: Enable tree-shaking and minification in build pipeline

Image Optimization Strategy

  • No images required: Pure CSS-based game board and X/O indicators
  • SVG fallbacks: If any icons needed, use inline SVGs with minimal paths
  • Lazy loading: Not applicable (no images)
  • Responsive sizing: CSS-based layout scaling

Font Loading Strategy

/* Preload critical font families */
@font-face {
  font-family: 'Inter';
  src: url('/fonts/inter-regular.woff2') format('woff2');
  font-display: swap;
}

/* Critical font stack */
body {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
}

Memoization Strategy

  • React.memo: Wrap GameBoard and Cell components to prevent unnecessary re-renders
  • useCallback: Memoize event handlers (handleCellClick, resetGame) to avoid recreating functions
  • useMemo: Cache game state calculations (win conditions, current player) during renders
  • Avoid over-memoization: Don't memoize simple props like string values or booleans

Virtualization for Large Lists

  • Not applicable: Tic-Tac-Toe has fixed 3x3 grid (9 cells total)
  • Alternative consideration: If expanding to larger boards, implement react-window for large grids

Prefetching and Preloading Strategy

  • Preload game assets: Critical CSS, font files, and game logic modules
  • Preconnect to APIs: If backend integration exists (not required for this feature)
  • Prefetch game state: When user navigates to game route, preload initial game state
  • Service worker caching: Cache static assets for offline availability (if needed)
// Example memoization implementation in component
const GameBoard = React.memo(({ cells, onCellClick }: GameBoardProps) => {
  return (
    <div className="game-board">
      {cells.map((cell, index) => (
        <Cell 
          key={index} 
          value={cell.value}
          onClick={() => onCellClick(index)}
        />
      ))}
    </div>
  );
});

const Cell = React.memo(({ value, onClick }: CellProps) => {
  const handleClick = useCallback(() => {
    onClick();
  }, [onClick]);

  return (
    <button 
      className="cell"
      onClick={handleClick}
      aria-label={`Cell ${value || 'empty'}`}
    >
      {value === 'X' ? 'X' : value === 'O' ? 'O' : ''}
    </button>
  );
});

7. Error Handling & Resilience

Error Boundary Placement Strategy

graph TD
    A[GameContainer] --> B[GameBoard]
    A --> C[GameStatus]
    A --> D[GameControls]
    B --> E[Square]
    B --> F[GameResult]
    E --> G[ErrorBoundary]
    F --> H[ErrorBoundary]

Error boundaries will be placed at the component level where individual game elements might fail:

  • GameBoard as a top-level error boundary for the entire board
  • Individual Square components wrapped in error boundaries to handle rendering failures
  • GameResult component wrapped for game outcome display errors

Global Error Handler Design

// src/lib/errorHandler.ts
import { captureException } from '@sentry/react';

export interface GlobalErrorConfig {
  logToConsole?: boolean;
  showUserFriendlyMessage?: boolean;
  captureInSentry?: boolean;
}

export class GlobalErrorHandler {
  static handle(
    error: Error,
    config: GlobalErrorConfig = {}
  ): void {
    const {
      logToConsole = true,
      showUserFriendlyMessage = true,
      captureInSentry = true
    } = config;

    if (logToConsole) {
      console.error('Tic-Tac-Toe Error:', error);
    }

    if (captureInSentry) {
      captureException(error);
    }

    if (showUserFriendlyMessage) {
      // Show toast or inline error message
      this.showUserError(error.message);
    }
  }

  private static showUserError(message: string): void {
    // Implementation for user-facing error display
    console.warn('User Error:', message);
  }
}

Offline / Network Failure Handling

The Tic-Tac-Toe game is a client-side only application, so network errors don't apply. However, we implement offline resilience:

// src/hooks/useOfflineDetection.ts
import { useState, useEffect } from 'react';

export const useOfflineDetection = () => {
  const [isOffline, setIsOffline] = useState(false);

  useEffect(() => {
    const handleOnline = () => setIsOffline(false);
    const handleOffline = () => setIsOffline(true);

    window.addEventListener('online', handleOnline);
    window.addEventListener('offline', handleOffline);

    return () => {
      window.removeEventListener('online', handleOnline);
      window.removeEventListener('offline', handleOffline);
    };
  }, []);

  return isOffline;
};

Graceful Degradation Patterns

// src/components/GameBoard.tsx
import React, { ErrorBoundary } from 'react';
import { useOfflineDetection } from '../hooks/useOfflineDetection';

interface GameBoardProps {
  board: (string | null)[];
  onSquareClick: (index: number) => void;
  winner: string | null;
}

const GameBoard: React.FC<GameBoardProps> = ({ 
  board, 
  onSquareClick,
  winner
}) => {
  const isOffline = useOfflineDetection();

  if (isOffline) {
    return (
      <div className="offline-message">
        <p>Offline mode detected. Game functionality limited.</p>
        <p>Please reconnect to play.</p>
      </div>
    );
  }

  return (
    <ErrorBoundary
      fallback={
        <div className="error-fallback">
          <p>Something went wrong rendering the game board.</p>
          <button onClick={() => window.location.reload()}>
            Reload Game
          </button>
        </div>
      }
    >
      {/* Actual board rendering */}
      <div className="game-board">
        {board.map((value, index) => (
          <Square
            key={index}
            value={value}
            onClick={() => onSquareClick(index)}
          />
        ))}
      </div>
    </ErrorBoundary>
  );
};

User Feedback on Errors

// src/components/ToastManager.tsx
import React, { useState, useEffect } from 'react';

interface Toast {
  id: string;
  message: string;
  type: 'error' | 'warning' | 'success';
  duration?: number;
}

const ToastManager: React.FC = () => {
  const [toasts, setToasts] = useState<Toast[]>([]);

  const addToast = (message: string, type: 'error' | 'warning' | 'success') => {
    const id = Date.now().toString();
    setToasts(prev => [...prev, { id, message, type }]);
    
    setTimeout(() => {
      setToasts(prev => prev.filter(t => t.id !== id));
    }, 5000);
  };

  const removeToast = (id: string) => {
    setToasts(prev => prev.filter(t => t.id !== id));
  };

  return (
    <div className="toast-container">
      {toasts.map(toast => (
        <div
          key={toast.id}
          className={`toast toast--${toast.type}`}
          role="alert"
        >
          {toast.message}
          <button
            onClick={() => removeToast(toast.id)}
            aria-label="Close toast"
          >
            ×
          </button>
        </div>
      ))}
    </div>
  );
};

export default ToastManager;

Error Recovery Strategy

The game implements automatic recovery from common error states:

  1. Invalid move detection: When an invalid move is attempted (e.g., clicking on a filled square), the game shows a clear error message and resets the move state.

  2. State corruption recovery: If game state becomes corrupted (e.g., invalid board configuration), the system will:

    • Log the error to Sentry
    • Show a "Game corrupted" message to user
    • Offer option to reset game state
  3. Component-level recovery: Individual component errors (like rendering failures) are isolated using React Error Boundaries to prevent cascading failures.

Critical Path Error Handling

// src/hooks/useGameLogic.ts
import { useState, useCallback } from 'react';
import { GlobalErrorHandler } from '../lib/errorHandler';

interface GameMove {
  index: number;
  player: 'X' | 'O';
}

export const useGameLogic = () => {
  const [board, setBoard] = useState<(string | null)[]>(Array(9).fill(null));
  const [currentPlayer, setCurrentPlayer] = useState<'X' | 'O'>('X');
  const [winner, setWinner] = useState<string | null>(null);
  const [isDraw, setIsDraw] = useState(false);
  const [error, setError] = useState<string | null>(null);

  const checkWinner = useCallback((boardState: (string | null)[]): string | null => {
    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
    ];

    for (const [a, b, c] of lines) {
      if (boardState[a] && 
          boardState[a] === boardState[b] && 
          boardState[a] === boardState[c]) {
        return boardState[a];
      }
    }

    return null;
  }, []);

  const makeMove = useCallback((index: number): void => {
    try {
      if (board[index] !== null) {
        throw new Error('Invalid move: Square already occupied');
      }

      if (winner || isDraw) {
        throw new Error('Game already completed');
      }

      const newBoard = [...board];
      newBoard[index] = currentPlayer;
      setBoard(newBoard);

      const newWinner = checkWinner(newBoard);
      if (newWinner) {
        setWinner(newWinner);
      } else if (newBoard.every(square => square !== null)) {
        setIsDraw(true);
      } else {
        setCurrentPlayer(currentPlayer === 'X' ? 'O' : 'X');
      }
    } catch (err) {
      GlobalErrorHandler.handle(err as Error, {
        showUserFriendlyMessage: true,
        captureInSentry: true
      });
      setError((err as Error).message);
    }
  }, [board, currentPlayer, winner, isDraw, checkWinner]);

  const resetGame = useCallback(() => {
    setBoard(Array(9).fill(null));
    setCurrentPlayer('X');
    setWinner(null);
    setIsDraw(false);
    setError(null);
  }, []);

  return {
    board,
    currentPlayer,
    winner,
    isDraw,
    error,
    makeMove,
    resetGame
  };
};

Error Recovery Flow

When an error occurs:

  1. The error is logged to Sentry for monitoring
  2. A user-friendly error message is displayed
  3. The game state is preserved to allow recovery
  4. Users can choose to reset the game or continue with current state

Resilience Testing Considerations

  • Simulate component rendering failures in Square components
  • Test error boundary behavior with various error types
  • Verify offline mode behavior and recovery
  • Validate toast notification system under different error conditions
  • Test state corruption scenarios and recovery paths

8. Testing Strategy

Unit Testing

  • Component tests: 90%+ coverage for all presentational and container components using React Testing Library
  • Utility function tests: 100% coverage for game logic utilities (win detection, board validation)
  • Custom hook tests: Full coverage of useGame hook with mocked state transitions and side effects
  • Test data: Comprehensive test cases including edge cases (draws, invalid moves, win conditions)
  • Target: 85%+ code coverage across all test suites

Integration Testing Strategy

  • Game flow integration: End-to-end component interaction tests (board state transitions, player turns)
  • State management integration: Verify proper hook behavior with mocked API responses and state updates
  • Cross-component communication: Test interactions between GameBoard, GameStatus, and MoveHistory components
  • Form validation integration: Test move submission flow with proper validation states

E2E Testing Scope and Tool

  • Tool: Playwright (better for modern React apps with real browser automation)
  • Covered flows:
    • Player moves and win detection
    • Game reset functionality
    • Multiplayer mode (if implemented)
    • Responsive behavior across device sizes
  • Test scenarios:
    • Complete game cycles (win/draw/loss)
    • Invalid move prevention
    • UI state consistency across interactions

Visual Regression Testing

  • Tool: Storybook with Chromatic or Percy
  • Scope: All component states (idle, active, win/draw, error)
  • Breakpoints: Mobile (375px), Tablet (768px), Desktop (1200px)
  • Test coverage: All component variants and state transitions

Accessibility Testing Automation

  • Tool: axe-core with Jest integration
  • Coverage: WCAG 2.1 AA compliance for all interactive elements
  • Tests:
    • Keyboard navigation completeness
    • ARIA attribute validity
    • Color contrast compliance (4.5:1 minimum)
    • Focus order consistency

Test Data Strategy and Fixtures

  • Test data:
    • Predefined board states (winning positions, draw scenarios)
    • Player move sequences with expected outcomes
    • Invalid input cases (out-of-bounds, invalid turns)
  • Fixtures:
    • JSON files for complex board states
    • Factory functions for generating test scenarios
    • Mock API responses for edge case testing

9. Accessibility (A11y)

WCAG 2.1 AA compliance checklist for this feature

The Tic-Tac-Toe game must meet WCAG 2.1 AA standards:

  • Text Contrast: All text must have a contrast ratio of at least 4.5:1 against its background (minimum 3:1 for large text)
  • Keyboard Navigation: All game interactions must be fully operable via keyboard
  • Focus Management: Clear focus indicators and proper focus order throughout the game interface
  • Screen Reader Support: Complete semantic structure for screen reader users
  • Error Identification: Clear error states and feedback for invalid moves
  • Input Labels: All interactive elements must have associated labels

Keyboard navigation support

All game interactions must be fully keyboard accessible:

  • Game Board: Navigation via arrow keys to move between cells (up, down, left, right)
  • Cell Selection: Enter/Space key to select a cell
  • Game Controls: Tab navigation through game controls (restart, undo, etc.)
  • Modal Interactions: Escape key to close modals and focus restoration

Focus management strategy

Focus Trapping: When game modals open (e.g., game over screen), focus is trapped within the modal until closed.

Focus Restoration: When modals close, focus returns to the last active element (e.g., game board or control button).

Focus Order: Logical tab order:

  1. Game controls (restart, undo)
  2. Game board cells (left-to-right, top-to-bottom)
  3. Modal close button (if applicable)

ARIA roles and attributes plan

// Game board structure with proper ARIA
<div 
  role="table" 
  aria-label="Tic-Tac-Toe game board"
  className="game-board"
>
  <div role="row" className="board-row">
    {cells.map((cell, index) => (
      <button
        key={index}
        role="gridcell"
        aria-label={`Cell ${index + 1}, ${cell ? cell : 'empty'}`}
        aria-pressed={cell !== null}
        onClick={() => handleCellClick(index)}
        className="board-cell"
      >
        {cell}
      </button>
    ))}
  </div>
</div>

// Game status with ARIA live region
<div 
  role="status" 
  aria-live="polite"
  className="game-status"
>
  {currentPlayer}'s turn
</div>

// Game over modal with ARIA
<div 
  role="dialog" 
  aria-labelledby="game-over-title"
  aria-modal="true"
  className="game-modal"
>
  <h2 id="game-over-title">Game Over</h2>
  <p>{winner ? `${winner} wins!` : 'Game ended in a draw!'}</p>
  <button 
    aria-label="Close game over modal"
    onClick={handleCloseModal}
  >
    Close
  </button>
</div>

Screen reader compatibility

  • Live Regions: Status updates use aria-live="polite" to announce changes without interrupting ongoing speech
  • Semantic HTML: Proper heading hierarchy, table structure for board, and grid roles
  • Dynamic Content Updates: Game state changes announce clearly to screen reader users
  • Alternative Text: All visual elements have appropriate text alternatives

Additional accessibility considerations

  • Reduced Motion Support: CSS prefers-reduced-motion media query to disable animations for users with motion sensitivity
  • High Contrast Mode: Support for OS-level high contrast themes
  • Focus Indicators: Visible, consistent focus rings that meet accessibility standards
  • Error Announcements: When invalid moves are attempted, screen readers announce the error clearly
// Example of reduced motion support in component
const GameBoard = () => {
  const [isReducedMotion, setIsReducedMotion] = useState(false);
  
  useEffect(() => {
    const mediaQuery = window.matchMedia('(prefers-reduced-motion: reduce)');
    setIsReducedMotion(mediaQuery.matches);
    
    const handleChange = (e: MediaQueryListEvent) => setIsReducedMotion(e.matches);
    mediaQuery.addEventListener('change', handleChange);
    
    return () => mediaQuery.removeEventListener('change', handleChange);
  }, []);

  return (
    <div 
      className={`game-board ${isReducedMotion ? 'reduced-motion' : ''}`}
      role="table"
      aria-label="Tic-Tac-Toe game board"
    >
      {/* Board cells */}
    </div>
  );
};

10. Security Considerations

XSS Prevention Strategy

Input Sanitization: All user inputs and game state modifications must be sanitized to prevent script injection. Since this is a frontend-only Tic-Tac-Toe game, we don't accept external user content that could be malicious, but we still sanitize any dynamic content rendered to the DOM.

// Example of input sanitization for game state display
const sanitizeGameMove = (move: string): string => {
  return move.replace(/[<>'"&]/g, (char) => {
    const escapeMap: Record<string, string> = {
      '<': '&lt;',
      '>': '&gt;',
      "'": '&#x27;',
      '"': '&quot;',
      '&': '&amp;'
    };
    return escapeMap[char];
  });
};

Content Security Policy (CSP): Implement strict CSP headers to prevent inline script execution and external resource loading:

Content-Security-Policy: default-src 'self'; script-src 'self' 'unsafe-inline'; style-src 'self' 'unsafe-inline';

CSRF Protection

Token Management: While not strictly necessary for a client-side game, we implement a basic CSRF token pattern to demonstrate security awareness:

// Token management for potential future API integration
const generateCSRFToken = (): string => {
  return Math.random().toString(36).substring(2, 15) + 
         Math.random().toString(36).substring(2, 15);
};

const validateCSRFToken = (token: string): boolean => {
  // In a real app, this would validate against server-side token store
  return typeof token === 'string' && token.length > 10;
};

Content Security Policy (CSP) Headers

For the Tic-Tac-Toe game, we recommend:

  • default-src 'self': Only allow content from the same origin
  • script-src 'self' 'unsafe-inline': Allow inline scripts (limited to game logic only)
  • style-src 'self' 'unsafe-inline': Allow inline styles for game styling
  • img-src 'self' data:: Allow local images and base64 encoded images

Sensitive Data Handling in Frontend State

Game State Protection: Game state should not contain sensitive information. However, we ensure that:

  • Player identifiers are not stored in local storage or cookies
  • Game state is not exposed to external services
  • No authentication tokens or session data are present in the game state
// Secure game state management - no sensitive data exposure
interface SecureGameState {
  board: (string | null)[];
  currentPlayer: 'X' | 'O';
  winner: string | null;
  isDraw: boolean;
  // No sensitive fields like user IDs, tokens, etc.
}

Input Sanitization Approach

Client-side validation: While server-side validation is crucial for real applications, client-side validation ensures user experience and prevents obvious malicious inputs:

// Validate game move coordinates
const validateMove = (row: number, col: number): boolean => {
  return Number.isInteger(row) && 
         Number.isInteger(col) && 
         row >= 0 && row < 3 && 
         col >= 0 && col < 3;
};

// Validate player moves
const validatePlayerMove = (move: string): boolean => {
  return ['X', 'O'].includes(move);
};

Security Best Practices Implementation

  1. No external dependencies: The game uses only first-party code to minimize security surface area
  2. Secure state transitions: All state changes are validated and controlled through defined actions
  3. Input validation: All user interactions are validated before processing
  4. No external API calls: The game is self-contained and doesn't make external HTTP requests
  5. Secure rendering: All DOM content is properly escaped to prevent XSS

Risk Mitigation

Risk: Potential for malicious state manipulation in local storage Mitigation: Game state is not persisted to localStorage or cookies in this implementation, and any persistence would require additional validation

Risk: Insecure game state sharing through URL parameters Mitigation: URL parameters are not used for game state management in this implementation

Risk: Client-side code injection Mitigation: No external libraries or dynamic code execution, all game logic is static and validated

Security Testing Considerations

  • Manual testing of input validation
  • Code review for potential XSS vulnerabilities
  • Review of all DOM manipulation for security issues
  • Ensure no sensitive data is exposed in console logs or error messages

The Tic-Tac-Toe game implementation follows security best practices for a client-side application, focusing on input validation and preventing common vulnerabilities while maintaining the simplicity and performance requirements of a basic game.

11. Internationalization (i18n)

Translation Key Structure and Naming Convention

Following BEM-style naming with feature-scoped keys:

// Translation keys structure
{
  "tictactoe": {
    "gameTitle": "Tic Tac Toe",
    "playerX": "Player X",
    "playerO": "Player O",
    "currentPlayer": "Current player: {{player}}",
    "winner": "Winner: {{player}}",
    "draw": "Game ended in a draw",
    "newGame": "New Game",
    "cell": "Cell {{position}}",
    "winningLine": "Winning line: {{line}}",
    "playerXWins": "{{player}} wins!",
    "playerOWins": "{{player}} wins!"
  }
}

RTL Support Considerations

RTL layout handling will be managed through CSS classes:

  • dir="rtl" attribute on root element when locale is RTL
  • CSS overrides for grid layout and text alignment
  • Button and modal positioning adjustments
  • Flip game board orientation in RTL mode

Date, Number, Currency Formatting Strategy

For any timestamp or numeric game stats:

  • Using Intl.NumberFormat for scores and move counts
  • Intl.DateTimeFormat for game session timestamps (if extended)
  • Consistent number grouping and decimal separators per locale

Locale Detection and Switching Strategy

Auto-detection:

  • Browser navigator.language as primary source
  • User preference stored in localStorage for persistence
  • Fallback to en-US if no supported language detected

Manual switching:

// Language switcher component
const LanguageSwitcher = () => {
  const [currentLocale, setCurrentLocale] = useState('en-US');
  
  const handleLocaleChange = (newLocale: string) => {
    setCurrentLocale(newLocale);
    i18n.changeLanguage(newLocale);
    localStorage.setItem('locale', newLocale);
  };
  
  return (
    <select 
      value={currentLocale} 
      onChange={(e) => handleLocaleChange(e.target.value)}
    >
      <option value="en-US">English</option>
      <option value="fr-FR">Français</option>
      <option value="ar-SA">العربية</option>
    </select>
  );
};

Supported languages:

  • English (en-US) - default
  • French (fr-FR) - for European markets
  • Arabic (ar-SA) - RTL support

Translation files:

src/locales/
├── en-US.json
├── fr-FR.json
└── ar-SA.json

i18next configuration:

import i18n from 'i18next';
import { initReactI18next } from 'react-i18next';

i18n
  .use(initReactI18next)
  .init({
    resources: {
      'en-US': { translation: enTranslation },
      'fr-FR': { translation: frTranslation },
      'ar-SA': { translation: arTranslation }
    },
    lng: localStorage.getItem('locale') || 'en-US',
    fallbackLng: 'en-US',
    interpolation: {
      escapeValue: false
    }
  });

RTL handling in CSS:

[dir="rtl"] .game-board {
  direction: rtl;
  text-align: right;
}

[dir="rtl"] .cell {
  transform: scaleX(-1);
}

i18n Implementation in Components

Game title and status messages will be translated:

import { useTranslation } from 'react-i18next';

const GameStatus = () => {
  const { t, i18n } = useTranslation('tictactoe');
  
  return (
    <div className="game-status">
      <h2>{t('gameTitle')}</h2>
      <p>{t('currentPlayer', { player: currentPlayer })}</p>
    </div>
  );
};

Testing i18n Implementation

  • Translation completeness checks for all supported languages
  • RTL layout verification in browser testing
  • Language switcher functionality end-to-end tests
  • Missing translation key detection during build process

Performance Considerations

  • Lazy loading of translation files
  • Preloading default language during app initialization
  • Bundling strategy to minimize impact on initial load time

Accessibility Considerations

  • Screen reader support for translated content
  • Proper ARIA labels that update with language changes
  • Focus management when switching between languages

12. Monitoring & Observability

Real User Monitoring (RUM) metrics to track

The Tic-Tac-Toe game should monitor key performance indicators that directly impact user experience:

  • First Input Delay (FID): Target <100ms for responsive gameplay
  • Largest Contentful Paint (LCP): Target <2.5s for board rendering
  • Cumulative Layout Shift (CLS): Target <0.1 for stable UI during moves
  • Time to Interactive (TTI): Target <5s for full game responsiveness
  • Session Duration: Track average play session length to identify engagement patterns

Error tracking integration

Integrate Sentry or Datadog for comprehensive error monitoring:

  • Game state corruption: Detect invalid board states or move sequences
  • Network errors: Track any API failures (if multiplayer features are added)
  • UI rendering errors: Capture React error boundaries for unexpected component failures
  • Performance degradation: Alert on frame drops or memory leaks during gameplay

Custom events and user journey tracking

Implement custom event tracking for key game interactions:

// Event tracking interface
interface GameEvent {
  eventName: string;
  timestamp: number;
  userId?: string;
  properties: Record<string, any>;
}

// Example tracking for user journey
const trackGameEvent = (event: GameEvent) => {
  // Send to analytics service (e.g., Mixpanel, Amplitude)
  console.log('Tracking event:', event);
};

Key events to track:

  • game_started: When a new game begins
  • move_made: Individual moves with position and player
  • game_won: Winner declaration with move count
  • game_drawn: Draw detection
  • player_switched: Turn changes (for multiplayer support)
  • board_reset: Game reset actions

Performance monitoring and regression detection

Implement performance baseline tracking:

// Performance monitoring hook
const usePerformanceMonitor = () => {
  const [metrics, setMetrics] = useState<PerformanceMetrics>({
    lcp: 0,
    fid: 0,
    cls: 0,
    tti: 0
  });

  useEffect(() => {
    // Collect core web vitals using Web Vitals API or Lighthouse
    const observer = new PerformanceObserver((list) => {
      list.getEntries().forEach((entry) => {
        setMetrics(prev => ({
          ...prev,
          [entry.name]: entry.value
        }));
      });
    });

    observer.observe({ entryTypes: ['largest-contentful-paint', 'first-input', 'layout-shift'] });
  }, []);

  return metrics;
};

Set up regression detection:

  • Alert on >10% increase in FID or LCP
  • Track daily average move processing time (should remain <50ms)
  • Monitor memory usage spikes during game state transitions

Feature-specific observability

For future extensibility, implement:

  • Multiplayer game tracking: Session duration, latency metrics for real-time play
  • AI opponent performance: Move decision time and win rate tracking (if AI is added)
  • User engagement metrics: Daily active users, retention rates, game completion rates

Data collection and privacy considerations

All monitoring data should:

  • Comply with GDPR/CCPA regulations
  • Anonymize user identifiers where possible
  • Provide opt-out mechanisms for analytics collection
  • Log only necessary performance and interaction data without personal information

Alerting strategy

Configure alerts for:

  • Critical: FID > 200ms, LCP > 3s, CLS > 0.25
  • Warning: TTI > 8s, memory usage > 100MB (for large boards)
  • Info: Game session duration > 30 minutes (potential engagement signal)

Dashboard visualization

Create dashboards for:

  • Real-time game performance metrics
  • User engagement heatmaps (move patterns)
  • Error rate trends over time
  • Feature adoption tracking (if multiple game modes are added)

This observability layer ensures the Tic-Tac-Toe game maintains high performance and user satisfaction while providing insights for future feature development and optimization.

13. File & Folder Structure

Proposed Directory Structure

src/
├── features/
│   └── tic-tac-toe/
│       ├── components/
│       │   ├── GameBoard.tsx
│       │   ├── GameStatus.tsx
│       │   ├── GameCell.tsx
│       │   └── GameControls.tsx
│       ├── hooks/
│       │   └── useGameLogic.ts
│       ├── types/
│       │   └── game.types.ts
│       ├── utils/
│       │   └── game.utils.ts
│       ├── index.ts
│       └── TicTacToeGame.tsx
├── shared/
│   ├── components/
│   │   ├── Button.tsx
│   │   ├── Modal.tsx
│   │   └── Spinner.tsx
│   ├── hooks/
│   │   └── useLocalStorage.ts
│   ├── styles/
│   │   └── theme.ts
│   └── utils/
│       └── validation.ts
├── app/
│   ├── routes/
│   │   └── TicTacToeRoute.tsx
│   └── App.tsx
└── index.ts

Module Boundaries and Import Rules

  • Feature isolation: Tic-Tac-Toe feature is completely self-contained with no cross-feature imports
  • Shared components: Only use shared components for generic UI elements (Button, Modal)
  • Barrel exports: Feature root index.ts exports all public components and hooks for clean imports
  • No circular dependencies: All internal feature modules can import from each other, but never from parent/child features

Shared vs Feature-Specific Code Separation

Shared code (reusable across features):

  • Generic UI components (Button, Modal)
  • Global hooks (useLocalStorage)
  • Theme and styling utilities
  • Validation helpers

Feature-specific code:

  • Game logic and state management
  • Tic-Tac-Toe specific components
  • Game-related types and utilities

This structure follows a feature-based organization that scales well for teams working on multiple features while maintaining clear module boundaries. The approach enables independent development, testing, and deployment of individual features without cross-contamination risks.

14. Dependencies & Third-Party Libraries

Key Libraries Selected with Justification

React (18.2+) - Core framework for building the UI components with hooks, context, and concurrent rendering capabilities. Essential for modern React development with features like automatic batching and suspense.

TypeScript (5.0+) - Static type checking for catching runtime errors early, better developer experience with autocompletion, and improved code maintainability across large teams.

React Testing Library (14.0+) - Best practices for testing React components by querying DOM elements as users would interact with them, ensuring accessibility and proper component behavior.

Jest (29.5+) - Industry-standard testing framework with built-in mocking capabilities, snapshot testing, and excellent TypeScript support for unit and integration tests.

React Router DOM (6.15+) - Declarative routing solution that enables client-side navigation, nested routes, and proper URL handling for the game's different views.

React Query (5.0+) - Production-ready data fetching and state management solution that handles caching, background updates, and optimistic UI patterns. While not strictly necessary for a simple Tic-Tac-Toe game, it provides excellent foundation for future extensibility.

Tailwind CSS (3.3+) - Utility-first CSS framework that enables rapid UI development with consistent design system, responsive behavior, and easy customization.

Framer Motion (10.16+) - Animation library that provides performant, declarative animations for game state transitions and user interactions without bloating the bundle.

Zustand (4.3+) - Lightweight state management solution that's simpler than Redux but more scalable than React Context, ideal for managing game state and player preferences.

Alternatives Considered and Rejected

Redux Toolkit - While powerful for complex state management, Tic-Tac-Toe's simple state model makes it overkill. Zustand provides similar functionality with less boilerplate and better TypeScript support.

MobX - Offers reactive programming but introduces complexity that's unnecessary for this feature. Zustand and React Query provide cleaner mental models for state management.

Styled Components - Provides CSS-in-JS capabilities but adds significant bundle size overhead and complexity. Tailwind's utility-first approach is more performant and maintainable for this use case.

Emotion - Similar to Styled Components, adds unnecessary complexity and bundle bloat for a simple game UI.

Licensing and Bundle Size Impact Assessment

React & React DOM: MIT license, ~150KB minified (core + hooks), essential for all React applications.

TypeScript: MIT license, no runtime impact - only affects build process.

Jest & React Testing Library: MIT license, no runtime impact - only development dependencies.

Tailwind CSS: MIT license, minimal runtime (20KB minified) with zero runtime dependencies. Bundle size is optimized through purge configuration.

Framer Motion: MIT license, ~20KB minified. Provides significant value for game animations without excessive overhead.

Zustand: MIT license, ~5KB minified. Lightweight alternative to Redux with minimal overhead.

React Router DOM: MIT license, ~15KB minified. Essential for routing and navigation in single-page applications.

Bundle Size Budget Impact

Total estimated runtime bundle size: ~200KB (minified + gzipped) for core functionality. This includes React, Tailwind CSS, Framer Motion, and Zustand - well within typical modern web application budgets.

All dependencies are actively maintained with regular security updates and have strong community support, making them suitable for production environments at FAANG scale.

15. Open Questions, Risks & Trade-offs

Decisions that need product/engineering alignment

  • Game state persistence: Should game progress be persisted across browser sessions? If yes, what storage mechanism (localStorage, IndexedDB) and sync strategy with backend?
  • Multiplayer support: Will this be a single-player vs. multiplayer game? If multiplayer, what's the backend architecture and real-time communication strategy (WebSockets vs. polling)?
  • Scoring system: Should there be a persistent leaderboard or just session-based win tracking?
  • Mobile-first vs. desktop-first: How should the UI adapt for touch vs. mouse interactions?

Known trade-offs made in this design

  • Simplified UI state management: Using React Context with local state for game state instead of Redux Toolkit or Zustand, trading global state complexity for simpler implementation and reduced bundle size
  • No AI opponent: For single-player mode, no complex AI logic is implemented to keep scope manageable and performance-focused
  • Static game board: Fixed 3x3 grid instead of configurable board sizes to simplify component architecture and reduce complexity
  • No game history/replay: No undo/redo functionality or game replay features to maintain clean, simple UX

Risks and mitigations

  • State synchronization issues: Risk of inconsistent game state between players if multiplayer support is added later. Mitigation: Implement proper server-side state management with optimistic updates and conflict resolution
  • Performance degradation: Large game boards or complex AI could cause performance issues. Mitigation: Use virtualization for large lists and memoization for expensive computations
  • Accessibility gaps: Risk of incomplete keyboard navigation or screen reader support. Mitigation: Comprehensive ARIA implementation and regular accessibility audits
  • Security vulnerabilities: XSS risks from user-generated content (if any). Mitigation: Strict input sanitization and proper HTML escaping

Technical debt acknowledged

  • Limited extensibility: Current design assumes single-player mode with fixed 3x3 board, making future feature additions more complex
  • No game configuration: No way to configure game rules (win conditions, board size) without major refactoring
  • Hardcoded UI logic: Game win condition checking and board state logic are tightly coupled to component rendering
  • No analytics integration: No built-in tracking for user behavior or game statistics collection, requiring future integration effort

Was this article helpful?

Topics in this article

More from Frontend LLD

Stay in the Loop

Get curated DSA patterns, coding problems, frontend and backend LLD/HLD, and AI/ML System Design insights delivered straight to your inbox. No spam, just focused interview and engineering content.