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:
- Setup for the tutorial will give you a starting point.
- Overview will teach you the fundamentals: components, props, and state.
- Completing the game will teach you the most common techniques in Kog development.
- Adding time travel will give you a deeper insight into Kog's unique strengths.
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:
- For the current move only, show "You are at move #..." instead of a button.
- Rewrite
Boardto use two loops to make the squares instead of hardcoding them. (Hint: nested.map()s over[0, 1, 2]— give the squares keys.) - Add a toggle button that lets you sort the moves in either ascending or descending order.
- 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. - 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.