All projects

Tic Tac Toe

A two-player Tic Tac Toe game with move history, score tracking and a fully accessible board

Tic Tac Toe

About the project

Tic Tac Toe – A Two-Player Game Built Around Derived State and Accessibility

A two-player game evolved from the official React tutorial into an architectural case study on derived state, side-effect isolation, and accessible UI engineering.

Overview

This project began as the standard Tic Tac Toe implementation from the official React documentation. A close code review and edge-case audit revealed a systemic design pattern flaw present in most beginner and intermediate implementations: storing derived data in state.

Refactoring the project transformed it into a technical case study demonstrating:

  1. Why state should be kept strictly minimal and derived during render.
  2. How to manage side effects cleanly inside event handlers instead of lifecycle hooks.
  3. How to build rich, animated, accessible components with zero external UI or animation dependencies.

Architectural Deep Dive: The Bug & The Fix

The Bug: Storing What Should Be Computed

In early iterations, the outcome of each round was stored directly in component state and written to during render:

[Render Phase] ---> evaluate board ---> setState(roundResult) ---> [Re-render Scheduled]

This pattern caused two major issues:

  1. Uncontrolled Re-renders: Triggering setState during rendering forces React to discard the current render pass and restart, an anti-pattern whose performance penalty is doubled under React's StrictMode.
  2. Broken Time-Travel: The roundResult state was never reset when navigating history. Once a game concluded, clicking back to an earlier move left the stale victory state active. The click handler continued to early-return, silently breaking the undo/replay feature.

The Fix: Derived State Architecture

The solution was eliminating outcome state entirely. At any point in time, the board's current array of squares contains all the data necessary to compute the game status:

  • Active Player: Derived from currentMove % 2 === 0.
  • Winner & Winning Line: Computed on the fly via a pure helper function (calculateWinner(squares)).
  • Draw State: Derived when all cells are non-null and no winner exists.
history + currentMove ---> currentSquares ---> calculateWinner() ---> { winner, winningLine, isDraw }

State vs. Derived Data Matrix

By restricting React state to values that cannot be inferred, the surface area for synchronization bugs drops to zero:

ValueCategorySource / Derivation
historyStateArray of board states representing the game history
currentMoveStateCurrent index pointing to the active position in history
scoresStateCumulative wins ($X$, $O$) and draws across multiple rounds
currentSquaresDerivedhistory[currentMove]
xIsNextDerivedcurrentMove % 2 === 0
winner / winningLineDerivedcalculateWinner(currentSquares) computed on each render
isDrawDerived!winner && currentSquares.every(Boolean)

Why Score Counting Cannot Be Derived

Unlike the active round status, cumulative score updates cannot be derived during render:

  • Incrementing a score is a discrete side effect of a completed round, not an inherent property of the current board layout.
  • If score calculations were executed inside a render pass or an effect tied to board state, stepping through the move history timeline would erroneously trigger score recountings.
  • Implementation: Score updates are dispatched exclusively within the cell play event handler at the exact moment a move produces a terminal outcome.

Key Features

1. Gameplay & Visual Feedback

  • Turn Alternation: Two players alternate marks on a $3 \times 3$ grid with $X$ consistently leading round one.
  • Victory Glow: The three winning cells are highlighted with a continuous pulsing glow immediately upon round completion.
  • Ghost Mark Preview: Hovering over or focusing on an available cell renders a semi-transparent ghost preview of the incoming player's mark.
  • SVG Stroke Animations: Custom vector $X$ and $O$ marks render using SVG stroke-dasharray and stroke-dashoffset keyframe animations for a fluid, hand-drawn appearance.

2. Move History & Time Travel

  • Interactive Timeline: Every board alteration commits a snapshot to a step-by-step move ledger.
  • Undo Tree Truncation: Making a move from an earlier timeline step discards future turns past that branch point, mirroring standard undo/redo history trees.
  • Assistive Tech Integration: The currently inspected turn is explicitly marked with aria-current="step" for screen readers.

3. Score Tracking & Confirmation Workflows

  • Session Persistence: Win tallies for $X$, $O$, and draws persist uninterrupted between consecutive rounds.
  • Safe Reset ("New Round"): Clears current board state and resets the timeline for the next game while preserving aggregate scores; requires no confirmation prompt.
  • Destructive Reset ("Reset Everything"): Completely wipes scores, history, and active marks. Protected by a native HTML <dialog> confirmation modal to prevent accidental data loss.

4. Deep Accessibility (a11y)

  • ARIA Grid Semantics: Built using standard grid accessibility roles (role="grid", role="row", and role="gridcell").
  • Descriptive Labels: Each board coordinate is an interactive <button> with dynamic, context-rich labels (e.g., "Row 2, column 3, empty" or "Row 1, column 1, X"), enabling full keyboard-only navigation via tab and arrow controls.
  • Live Status Announcements: Game state changes (turn transitions, declarations of victory, and draws) are piped through an aria-live="polite" region.
  • Motion Sensitivities: All CSS keyframe transitions and stroke draw animations strictly honor @media (prefers-reduced-motion: reduce).
  • Adaptive Layout: Fully responsive layout tested across viewports ranging from 320px mobile screens to wide desktop monitors.

Why This Project Stands Out

  1. Zero Runtime Dependencies: Built strictly with React, native browser APIs, and standard CSS. No external component suites, icon packs, or animation libraries are required.
  2. Decoupled Business Logic: Pure game-rule mechanics sit entirely decoupled from the React rendering layer in an isolated utility returning typed game outcomes (winner identifier + cell coordinate indices).
  3. Intentional State Footprint: Avoids synchronization pitfalls by deriving all transient states directly during the render phase.
  4. Native Dialog Primitives: Leverages the native <dialog> element for modal workflows, providing automatic focus trapping, escape-key closing, and backdrop handling without third-party portal libraries.

Let's work together

Have a role or a project in mind?

I'm open to full-stack and frontend roles, remote or in Dhaka, Bangladesh. Send a message and I'll reply within a day.

Tic Tac Toe | Md. Samiul Karim Prodhan