Skip to main content

Tutorial: Tic-Tac-Toe

You will build a small tic-tac-toe game during this tutorial. It is a deliberate, 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 one that runs on an ESP32 — compiled to WebAssembly, and 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 — with 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 — that's the entire contract.

:::note Text children are text A <Text> accepts strings, numbers, and expressions — 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:

A Kog component must return a single JSX element.

React's version of this error tells you to wrap siblings in a Fragment (<>...</>). Kog has no Fragments: the widget tree is retained — every element is a real native object with a real parent — so there is no such thing as elements without a container. Wrap them in a <View> instead. Far from being a workaround, that View is about to earn its keep: 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.)

:::note 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 — <Square {...props} /> — are the one prop syntax v1 rejects, and the compiler will tell you so.) :::

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 onClick of the touch world:

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 quietly different:

:::info No re-render — divergence #3 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. :::

:::note 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. (Watch the editor: TypeScript flags the un-defaulted version too.) :::

Completing the game

You now have the building blocks. Next: put X's and O's on the board, and alternate turns — which 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 type does triple duty for 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 — nine tiny reads — and the label whose text changed repaints. 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 the same idiom is load-bearing. 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. This is the contract fine-grained reactivity trades on: a changed value is a new value. The payoff is the same one React promises — cheap updates for everything that didn't change, plus, later in this tutorial, an entire time-travel feature powered by kept-around old boards — but in Kog the discipline is enforced by the engine, not by 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);
}

:::info 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;
}

Here comes the biggest Kog moment on this page. React's tutorial now writes, directly in the component body:

// React version — DOES NOT track in Kog
const winner = calculateWinner(squares);
let status;
if (winner) { ... }

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). Written this way, winner is computed exactly once — against the empty board — and stays null forever. A derived value that should stay current is declared with useMemo:

const winner = useMemo(() => calculateWinner(squares), [squares]);
const status = useMemo(
() => (winner ? 'Winner: ' + winner : 'Next player: ' + (xIsNext ? 'X' : 'O')),
[winner, xIsNext],
);

In React, useMemo is a performance hint you could delete. In Kog it's the primitive for "this value is derived from that value": a memo re-computes when the signals it reads change, and anything reading the memo updates in turn. (The deps array is accepted for React compatibility, but tracking is automatic — a forgotten dep can't go stale.) handleClick needs no such treatment: 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. And you've just used the one rule that separates a Kog component from a React one: body code runs once; derived values are memos; 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 = useMemo(() => history[history.length - 1], [history]);

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

Note currentSquares: React derives it with a bare const currentSquares = history[history.length - 1] in the body. Same story as winner — in Kog that's a one-shot read, so a derived value is a useMemo. The screen layout changes too: Game's root is a flexDirection: 'row' View — 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. 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 @kog/eslint-plugin flags it in a real project, and 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, 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 = useMemo(() => history[currentMove], [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 memo recomputes, and the nine square labels rebind in place — no screen rebuild, no list churn, 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. In React the fix is a plain body expression; in Kog — you know the rule by now — a derived value is a memo:

const xIsNext = useMemo(() => currentMove % 2 === 0, [currentMove]);

That's the finished game — 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 — same source, byte-for-byte same behavior.

Wrapping up

You've built a tic-tac-toe game with lifted state, a derived winner, and time travel — and met every place Kog's run-once, fine-grained engine diverges from React on the way: body code runs once (derive with useMemo), state reads are synchronous, immutability is enforced by ===, lists are keyed rebinding regions, and there are no Fragments because every element is a real widget.

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 — and 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.