Skip to main content

Tutorial: Tic-Tac-Toe

You will build a small tic-tac-toe game during this tutorial. It is an intentionally close, nearly line-for-line port of React's classic tic-tac-toe tutorial: same components, same state design, same order of teaching. If you've done that one, you'll feel at home; if you haven't, this is a complete introduction on its own.

The twist is what's underneath. Every playground on this page runs the real Kog runtime (the same one that runs on an ESP32) compiled to WebAssembly. Kog has no virtual DOM and no re-renders. Your component runs once, and afterwards only the expressions that read changed state re-execute. Mostly you won't notice. But the handful of places where the React tutorial leans on "the component re-renders" are exactly the places Kog works differently, and this page stops at each one. If you want the theory first, read Thinking in Kog; this tutorial is the practice.

The tutorial is divided into several sections:

What are you building?​

In this tutorial, you'll build an interactive tic-tac-toe game with a move list and time travel. Here's the finished game — play it now:

Tap squares to play, and use the list on the right to jump to any earlier move. The whole thing fits a 320x240 display: this is the shipped examples/tic-tac-toe app, which you can kog flash to a real board when you're done.

Setup for the tutorial​

There is nothing to set up. In React's tutorial you fork a CodeSandbox; here, every code block on this page is the editor. Each playground compiles your edits in a web worker and hot-reloads them into the simulator beside it in milliseconds, using the same compiler and the same runtime as kog build.

If you'd rather follow along locally, npx create-kog@latest tic-tac-toe gives you a project and npm run dev gives you the desktop simulator; see Installation. Everything below works identically in both places.

Overview​

Inspecting the starter code​

Start with the smallest possible piece of the game — one square:

There is a single file, App.tsx, and it holds everything the React starter spreads across three:

The component. Square is a plain function returning JSX, exported as the default. Where React's starter returns <button className="square">X</button>, Kog returns a <Pressable> wrapping a <Text>: there is no HTML on a microcontroller, so JSX elements are native LVGL widgets. A Pressable is a touchable object; a Text is a label.

The styles. There's no styles.css; the .square CSS class becomes an entry in StyleSheet.create. Layout is real flexbox (LVGL has a flex engine), numbers are pixels, and the whole sheet is resolved at build time into shared native styles.

No index.js. Nothing calls createRoot. The runtime mounts your default export as the screen, and that's the entire contract.

Text children are text

A <Text> accepts strings, numbers, and expressions, but never nested elements. LVGL labels are leaf widgets, and the compiler enforces it.

Building the board​

A board needs nine of these. Try it: in the playground above, paste a second square right after the first:

export default function Square() {
return (
<Pressable style={styles.square}>
<Text style={styles.squareText}>X</Text>
</Pressable>
<Pressable style={styles.square}>
<Text style={styles.squareText}>X</Text>
</Pressable>
);
}

The error panel shows:

Adjacent JSX elements must be wrapped in an enclosing tag. Did you want a JSX fragment <>...</>?

That's the same parser error React gives, and it suggests a Fragment (<>...</>). Kog supports fragments, so the hint would work, but here a <View> is the better wrapper. The widget tree is retained: every element is a real native object with a real parent, and this View is about to prove useful, because it's your screen and it lays out the board.

Since this component is no longer a single square, rename it to Board, group the squares into three <View style={styles.boardRow}> rows of three, and number the squares so we can see the layout. Two style notes as you do: containers stack children in a column by default (like React Native), so each row sets flexDirection: 'row'. And the screen View takes width/height: '100%' plus a background. It's the display now.

Passing data through props​

Nine copy-pasted squares are already painful, and next we'll want each one to show its own value. Time for props. Bring back Square, taking value as a prop, and have Board pass it:

function Square({ value }: { value: string }) {
return (
<Pressable style={styles.square}>
<Text style={styles.squareText}>{value}</Text>
</Pressable>
);
}

{value} in the JSX escapes from markup into JavaScript, exactly as in React. One Kog note: this is TSX, and the playground's editor runs the same strict TypeScript that kog build gates on, so props are typed. (Also as in React Native, there's no className; styles attach via style.)

Destructured props stay live

In some fine-grained frameworks, destructuring props breaks reactivity. Not in Kog: the compiler rewrites every read of value back to the live prop, so destructure freely. (Spread props aren't supported in v1. On a built-in element like <Text {...props} /> the compiler rejects them, but on your own component (<Square {...props} />) they are currently dropped without an error, so pass props explicitly.)

Making an interactive component​

Let's make a square respond to taps. First, prove the wiring: give Square a handler and pass it to onPress (the touch world's onClick):

function Square({ value }: { value: string }) {
function handleClick() {
console.log('clicked!');
}

return (
<Pressable style={styles.square} onPress={handleClick}>
<Text style={styles.squareText}>{value}</Text>
</Pressable>
);
}

Try this edit in the playground above and tap a square: the log shows up in the console panel under the playground, forwarded from the runtime.

Now make the square remember it was tapped. Memory is useState, imported from 'react'. Kog keeps React's hook names and signatures:

Each square starts empty and shows an X once tapped, and each Square has its own independent state. So far this is the React tutorial verbatim. Two things are subtly different:

No re-render (divergences #3 and #5)

React's tutorial says: "calling the setter re-renders the component." Kog never re-runs Square. setValue('X') updates a signal, and the one expression that reads it (the Text child) runs its one native setter. That's the entire cost of a tap, no matter how big the app around it gets.

Why {value ?? ''} and not {value}?

Text children concatenate into a single label string. React renders null as nothing; a label prints it as the word null. The ?? '' keeps an empty square empty.

Completing the game​

You now have the building blocks. Next: put X's and O's on the board, and alternate turns. That means the squares can no longer keep private state.

Lifting state up​

To decide whose turn it is (and later, who won), some single place must know the whole board. Nine independent square states can't be inspected from outside, so the state moves up into Board: one array of nine values,

const [squares, setSquares] = useState<SquareValue[]>(Array(9).fill(null));

where SquareValue is 'X' | 'O' | null. The one type covers a square, a winner, and "nothing yet". Square becomes a controlled component again: it receives its value and reports taps through an onSquareClick prop, both handed down by Board:

<Square value={squares[0]} onSquareClick={() => handleClick(0)} />

and handleClick updates the board with a copy:

function handleClick(i: number) {
const nextSquares = squares.slice();
nextSquares[i] = 'X';
setSquares(nextSquares);
}

Every tap writes an X for now; turns come next. Notice what an update does under the hood: squares[0] inside a JSX prop is a reactive expression, so when the array signal changes, each square re-evaluates its own slot and re-sets its label (nine tiny reads and nine cheap label writes). React re-renders Board, rebuilds every element, and diffs the result; Kog never touches the components at all, just the bindings that read the array.

Why immutability is important​

The React tutorial pauses here to argue for .slice() (copying instead of mutating) as good practice that pays off later. In Kog, correctness depends on that idiom. Try the mutating version:

function handleClick(i: number) {
squares[i] = 'X'; // mutate in place...
setSquares(squares); // ...and set the same array back
}

Nothing happens on screen. A signal compares by reference (===); handed the same array object, it sees no change and updates nothing. Fine-grained reactivity depends on this contract: a changed value is a new value. The payoff is the same one React promises: cheap updates for everything that didn't change, and (later in this tutorial) a whole time-travel feature built on kept-around old boards. The difference is that in Kog the engine enforces the discipline; in React it's a convention.

Taking turns​

Time for O. Track whose turn it is with a second piece of state, skip already-filled squares, and flip the turn on each move:

function handleClick(i: number) {
if (squares[i]) {
return;
}
const nextSquares = squares.slice();
nextSquares[i] = xIsNext ? 'X' : 'O';
setSquares(nextSquares);
setXIsNext(!xIsNext);
}
State reads are live (divergence #2)

At this point React's tutorial explains snapshots: in React, setXIsNext(!xIsNext) doesn't change what xIsNext returns until the next render. Kog is the opposite: state writes are synchronous. On the line after setXIsNext(!xIsNext), reading xIsNext gives the new value; only the visual flush batches to a microtask. The code above works in both models, but the habit of "set then read the old value" is the #1 React reflex to unlearn. When a handler needs a value after setting it, capture it first: const next = !xIsNext; setXIsNext(next);.

Declaring a winner​

The game needs to know when it's over. Winning at tic-tac-toe means checking eight lines. Copy calculateWinner from the React tutorial unchanged (it's plain JavaScript; paste it below the component):

function calculateWinner(squares: SquareValue[]): SquareValue {
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;
}

This next step shows Kog's model more clearly than any other on this page. React's tutorial now writes, directly in the component body:

const winner = calculateWinner(squares);
let status;
if (winner) {
status = 'Winner: ' + winner;
} else {
status = 'Next player: ' + (xIsNext ? 'X' : 'O');
}

That works in React because the body re-runs on every render, recomputing winner each time. A Kog component body runs once, at mount (divergence #3). So how can winner ever change? Because the compiler reads the derivation: winner is a pure computation from squares, read by the UI, so it is kept live automatically. Under the hood it becomes a memo that recomputes when squares changes, exactly as if the body had re-run. The const winner line ports verbatim.

The let status half is the part that doesn't. Keeping an imperatively-assigned variable current would mean silently re-running arbitrary statements behind your back, and that is the one thing Kog never does. So instead of letting the value freeze silently, Kog refuses with an error:

'status' is computed imperatively from reactive state, but a Kog component body runs ONCE — the assignments to 'status' run a single time at mount… Derive it as a single pure expression (the compiler keeps those live automatically), wrap the computation in useMemo(() => …), or make it state with useState if events update it.

The fix is the expression form of the same logic:

const winner = calculateWinner(squares);
const status = winner ? 'Winner: ' + winner : 'Next player: ' + (xIsNext ? 'X' : 'O');

useMemo(() => calculateWinner(squares), [squares]) also still works. It's the same memo the compiler builds for you, spelled out, and some teams prefer derived values marked explicitly. (The deps array is accepted for React compatibility, but tracking is automatic, so a forgotten dep can't go stale.) handleClick needs no treatment at all: handlers always read current state, so calling calculateWinner(squares) inline there is already correct.

Add a status label above the board, and block moves once someone has won:

Congratulations, you have a working tic-tac-toe game. You've also met the rule that separates a Kog component from a React one: body code runs once; pure derivations stay live automatically; anything the compiler can't safely re-run is a compile error, never a silent freeze; handlers are always live.

Adding time travel​

As a final exercise, let's make it possible to "go back in time" to any earlier move.

Storing a history of moves​

Because handleClick builds a new array for every move (this is immutability paying off), old boards are never destroyed, just abandoned. Keep them instead: store an array of boards,

[
[null, null, null, null, null, null, null, null, null],
[null, null, null, null, null, null, null, 'X', null],
['O', null, null, null, null, null, null, 'X', null],
]

and the game state becomes that history array plus which entry is showing.

Lifting state up, again​

Just as square state moved up into Board, board state now moves up into a new top-level Game component, so it can own the history and the move list. Board becomes fully controlled: xIsNext, squares, and an onPlay callback all arrive as props:

export default function Game() {
const [xIsNext, setXIsNext] = useState(true);
const [history, setHistory] = useState<SquareValue[][]>([Array(9).fill(null)]);
const currentSquares = history[history.length - 1];

function handlePlay(nextSquares: SquareValue[]) {
setHistory([...history, nextSquares]);
setXIsNext(!xIsNext);
}
...

Note currentSquares: a bare body read of history, exactly as React writes it. As with winner, it's a pure derivation, so the compiler keeps it live. The screen layout changes too: Game's root is a flexDirection: 'row' View, with the board on the left and a ScrollView on the right that will hold the move list. This is a 320x240 display, so the "page" React lays out in CSS becomes one carefully budgeted screen.

Showing the past moves​

The history is an array, and rendering arrays works like React: map entries to elements, with one structural difference. React's tutorial builds the list in the component body (const moves = history.map(...)) and drops {moves} into the JSX. In Kog, write the .map() inline in the JSX: the compiler turns that expression into a keyed, reconciled list region, and it can only do that where the map is statically visible. For the same reason, the map callback should return an element, not run statements. So where React builds description with an if/else, use a ternary:

<ScrollView style={styles.gameInfo}>
{history.map((squares, move) => (
<Pressable style={styles.move} onPress={() => jumpTo(move)}>
<Text style={styles.moveText}>
{move > 0 ? 'Go to move #' + move : 'Go to game start'}
</Text>
</Pressable>
))}
</ScrollView>

Play a few moves. A button appears in the list for each one (jumpTo is still a TODO). At this exact point the React tutorial's console shows its famous warning: "Each child in a list should have a unique 'key' prop." Kog's runtime doesn't warn (with no key it silently keys rows by index), but the reason to care is the same. So:

Picking a key​

A key tells the list reconciler which row is the same row across updates. That matters because rows aren't re-rendered; they're rebound. When history changes, the reconciler matches new entries to existing rows by key: matched rows update their signals in place (the widget survives, along with any state and scroll position), unmatched keys mount, and missing keys dispose. An index key breaks down exactly when React's does: if the list can reorder, insert, or delete in the middle, identity follows the position instead of the data. This move list only ever appends (and, soon, truncates from the end), so the move number is a perfectly stable key:

<Pressable key={move} style={styles.move} onPress={() => jumpTo(move)}>

One extra Kog rule from the referential-equality contract: when a row's data changes, supply a new object — rows compare items by ===, the same rule you met in "Why immutability is important."

Implementing time travel​

To show an older board, track which move is selected (currentMove) and make it real state in Game:

const [currentMove, setCurrentMove] = useState(0);
const currentSquares = history[currentMove];

function handlePlay(nextSquares: SquareValue[]) {
const nextHistory = [...history.slice(0, currentMove + 1), nextSquares];
setHistory(nextHistory);
setCurrentMove(nextHistory.length - 1);
setXIsNext(!xIsNext);
}

function jumpTo(nextMove: number) {
setCurrentMove(nextMove);
setXIsNext(nextMove % 2 === 0);
}

handlePlay now truncates history when you play from an earlier move: jump back two moves, play, and the abandoned future is sliced away before the new board is appended. Watch currentSquares do its job: jumpTo sets one signal, the derivation recomputes, and the nine square labels rebind in place. There's no screen rebuild, no list churn, and no remounting. The list, meanwhile, only disposes the rows whose keys vanished.

Final cleanup​

One smell remains: xIsNext is stored state, but it's fully determined by currentMove (even moves are X's). Stored copies of derivable facts drift out of sync; derive it instead and both setXIsNext calls disappear. The fix is the same plain body expression as in React. It's a pure derivation, so the compiler keeps it live:

const xIsNext = currentMove % 2 === 0;

That's the finished game. It's the same code as the playground at the top of this page, shipped as examples/tic-tac-toe. From that directory, npm run dev opens it in the desktop simulator and kog flash puts it on a real board, with the same source and byte-for-byte the same behavior.

Wrapping up​

You've built a tic-tac-toe game with lifted state, a derived winner, and time travel. On the way you met every place Kog's run-once, fine-grained engine diverges from React: body code runs once (pure derivations stay live automatically; impure ones are compile errors, not silent staleness), state reads are synchronous, immutability is enforced by ===, lists are keyed rebinding regions, and every element is a real widget (a fragment groups widgets without adding one, but it can't be a list row).

If you have extra time or want to practice your new skills, here are some ideas for improvements, listed in order of increasing difficulty:

  1. For the current move only, show "You are at move #..." instead of a button.
  2. Rewrite Board to use two loops to make the squares instead of hardcoding them. (Hint: nested .map()s over [0, 1, 2]; give the squares keys.)
  3. Add a toggle button that lets you sort the moves in either ascending or descending order.
  4. When someone wins, highlight the three squares that caused the win (a conditional style array like style={[styles.square, isWinning && styles.winningSquare]} does it). When no one wins, display a message about the result being a draw.
  5. Display the location for each move in the format (row, col) in the move history list.

From here, read Thinking in Kog for the full theory of the six divergences, continue with Learn by Example, or take this game to hardware with Flash your board.


This page mirrors the structure of react.dev's Tic-Tac-Toe tutorial (CC BY 4.0), adapted for Kog's engine, widgets, and displays.