Adding Time Travel

Store the history of moves and let players jump back to previous states.

Lifting State Up

To implement time travel, we need to store the history of all moves. This means the Game component needs to own the state, not the Board.

When multiple components need to share state, we "lift" it to their closest common parent. This is called lifting state up.

src/main.jsx
import './styles.css';
import { signal, computed } from 'what-framework';

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 (const [a, b, c] of lines) {
    if (squares[a] && squares[a] === squares[b] && squares[a] === squares[c]) {
      return squares[a];
    }
  }
  return null;
}

function Square({ value, onClick }) {
  return (
    <button className="square" onClick={onClick}>
      {value}
    </button>
  );
}

// Board now receives squares and onPlay as props
function Board({ squares, onPlay }) {
  return (
    <div className="board">
      {() => squares().map((value, i) => (
        <Square key={i} value={value} onClick={() => onPlay(i)} />
      ))}
    </div>
  );
}

export default function Game() {
  // History is an array of board states
  const history = signal([Array(9).fill(null)]);
  const currentMove = signal(0);

  // Current squares derived from history and currentMove
  const currentSquares = computed(() =>
    history()[currentMove()]
  );

  // X plays on even moves (0, 2, 4...)
  const xIsNext = computed(() =>
    currentMove() % 2 === 0
  );

  const status = computed(() => {
    const winner = calculateWinner(currentSquares());
    if (winner) return `Winner: ${winner}`;
    return `Next player: ${xIsNext() ? 'X' : 'O'}`;
  });

  function handlePlay(i) {
    const squares = currentSquares();
    if (calculateWinner(squares) || squares[i]) return;

    const next = [...squares];
    next[i] = xIsNext() ? 'X' : 'O';

    // Add new state to history (slice to discard future if we jumped back)
    const newHistory = [...history().slice(0, currentMove() + 1), next];
    history.set(newHistory);
    currentMove.set(newHistory.length - 1);
  }

  function jumpTo(move) {
    currentMove.set(move);
  }

  return (
    <div className="game">
      <h1>Tic-Tac-Toe</h1>
      <div className="status">{status}</div>
      <Board squares={currentSquares} onPlay={handlePlay} />
      <div className="game-info">
        <div className="moves">
          {() => history().map((_, move) => (
            <button key={move} onClick={() => jumpTo(move)}>
              {move === 0 ? 'Go to game start' : `Go to move #${move}`}
            </button>
          ))}
        </div>
      </div>
    </div>
  );
}

How Time Travel Works

1

History Array

history stores every board state. It starts with one entry: an empty board.

2

Current Move Index

currentMove tracks which move we're viewing. When we make a move, it's the last index. When we jump back, it points to an earlier state.

3

Computed Current Squares

currentSquares is derived from history and currentMove. Change either and the board updates.

4

Making a Move

When you click a square, we slice the history up to the current move (discarding any "future" moves if we jumped back), then add the new state.

5

Jumping to a Move

Clicking a history button just updates currentMove. The computed values handle the rest!

What's Happening Here

Immutability Enables Time Travel

Because we always created new arrays instead of mutating, we have a complete history of every board state. This is a powerful pattern that enables features like undo/redo, debugging, and replay.

Congratulations!

You've built a complete tic-tac-toe game with:

  • Signals — Reactive state that updates the UI
  • Computed values — Derived state that stays in sync
  • Components — Reusable UI pieces
  • Event handling — User interactions
  • Lifting state — Sharing state between components
  • Immutable updates — Enabling time travel

What's Next?

Here are some ideas to improve the game:

  • Highlight the winning squares
  • Add a reset button
  • Show "It's a draw" when the board is full with no winner
  • Save game state to localStorage

Or dive deeper into What Framework: