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, and calling the signal with an argument is the same write, so count(value) and count(c => c + 1) also work (that is the form the generated starter file uses). Updates belong in an event handler or an effect, because that is where something actually changes.

Components run once

A What component body runs exactly once. There is no re-render, so a .set() sitting in the body is just a one-time assignment that happens before the first paint: harmless, but pointless. Give signal() the value you wanted instead.

// Pointless: the body runs once, so just start at 1
function Odd() {
  const count = signal(0);
  count.set(1);
  return <p>{count()}</p>;
}

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

The write that really does bite is one inside a computed(). That function re-runs whenever its dependencies change, so a write in there can feed itself. What logs a warning when it sees one.

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 { mount, 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>
  );
}

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

mount(<Game />, '#app');

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

Writing {squares().map(...)} without the arrow still works here, because the What compiler wraps any JSX expression that reads a signal for you. It does not work in code the compiler never sees, such as hand-written h() calls in a buildless app, where an unwrapped read is evaluated once and never updates again. Writing the arrow yourself keeps the binding reactive either way.

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!