Adding State

Use signals to track the game state and make squares interactive.

Introducing Signals

Right now our squares don't remember anything. We need state — data that changes over time and updates the UI.

In What Framework, we use signals for state. A signal is a container for a value that can change. When the value changes, any part of the UI that uses it updates automatically.

import { signal } from 'what-framework';

function Counter() {
  // Create a signal with initial value
  const count = signal(0);

  return (
    <div>
      <p>Count: {count()}</p>
      // Update signals in event handlers
      <button onClick={() => count.set(count() + 1)}>+1</button>
      <button onClick={() => count.set(0)}>Reset</button>
    </div>
  );
}

signal(0) creates the signal. count() reads it. count.set(value) updates it — always inside an event handler or effect, never bare in the component body.

Never call .set() in the component body

Calling .set() directly in a component function (outside event handlers or effects) causes an infinite loop — the update triggers a re-render, which calls .set() again, forever.

// BAD — infinite loop!
function Broken() {
  const count = signal(0);
  count.set(1);  // Re-renders → calls set again → forever
  return <p>{count()}</p>;
}

// GOOD — update in response to events
function Works() {
  const count = signal(0);
  return <button onClick={() => count.set(1)}>Set</button>;
}

Adding State to the Board

Let's create a signal to hold the state of all 9 squares:

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

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

function Board() {
  // Array of 9 squares, each null (empty) initially
  const squares = signal(Array(9).fill(null));

  function handleClick(i) {
    // Create a copy of the squares array
    const next = [...squares()];
    // Mark this square as "X"
    next[i] = 'X';
    // Update the signal
    squares.set(next);
  }

  return (
    <div className="board">
      {() => squares().map((value, i) => (
        <Square
          key={i}
          value={value}
          onClick={() => handleClick(i)}
        />
      ))}
    </div>
  );
}

export default function Game() {
  return (
    <div className="game">
      <h1>Tic-Tac-Toe</h1>
      <Board />
    </div>
  );
}

Try clicking squares now. Each one you click shows an "X"!

Understanding Reactive Rendering

Notice this line in the Board:

{() => squares().map((value, i) => ...

The arrow function () => is important. It tells What to:

  1. Track that this part of the UI depends on squares
  2. Re-run this function whenever squares changes
  3. Update only this part of the DOM

Common Mistake

If you write {squares().map(...)} without the arrow function, it won't update when the signal changes. The arrow function makes it reactive.

Why Copy the Array?

In handleClick, we create a copy of the array instead of modifying it directly:

// Good - create a copy
const next = [...squares()];
next[i] = 'X';
squares.set(next);

// Bad - mutating directly won't work
const current = squares();
current[i] = 'X';  // Mutation - What won't detect this change

This is called immutability. When you create a new array, What knows something changed. If you mutate the existing array, it looks like the same object and What won't update the UI.

Checkpoint

Test your game:

  • Click a square — it should show "X"
  • Click another square — it should also show "X"
  • You can fill the whole board with X's

But wait... we can only place X's, and we can overwrite squares. Let's fix that next!