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:
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:
- Track that this part of the UI depends on
squares - Re-run this function whenever
squareschanges - 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!