Thinking in Kog
Kog looks like React because it is React at the surface: TSX, hooks, the component mental model. But the engine underneath is fine-grained reactivity, not a virtual DOM — your component function runs once, at mount, and afterward only the specific expressions that depend on changed state re-execute.
This is almost always what you wanted React to do anyway. But it produces six observable differences. Learn these and you know everything unusual about Kog.
The six divergences
1. No stale closures
Event handlers always read the current value of state. count inside onPress evaluates when the handler runs, not when the component "rendered" (there is no render to capture it).
const [count, setCount] = useState(0);
// In React this logs the count from render time. In Kog it logs the count *now*.
<Pressable onPress={() => console.log(count)} />
The functional-update form setCount(c => c + 1) still works — it's just no longer required for correctness.
2. Reads after setState see the new value
State writes are synchronous; the UI flush is batched to a microtask.
setCount(1);
console.log(count); // 1 in Kog (0 in React)
The gotcha: the #1 useState-habit slip is reading a value after setting it and expecting the old one — setCount(n); log(count + 1) logs n + 2, not n + 1, because count is already n. Capture the value up front: const next = count + 1; setCount(next); log(next);.
3. The component body runs once
Code directly in the component body executes at mount, once. It is not "the render function" — there is no render loop to re-run it. What keeps derived values fresh is that the compiler reads your derivations: a binding computed purely from state, props, a @kog/router location hook (const { pathname } = useLocation()), or another derived binding, and read by something reactive, is kept live automatically. State/props derivations use one memo when read in several places and an inlined recompute when read once; direct router-hook derivations retain an owner-bound memo so they also work from native event callbacks. So const passed = results.filter(r => r.ok).length tracks results exactly as it would in React, with no annotation.
What the compiler cannot safely keep live, it refuses to compile — loudly, naming the frozen binding — rather than let it freeze silently: initializers it can't prove side-effect-free (re-running a log, a mutation, or an unknown call behind your back would change what your program does), and variables assigned imperatively (let status; if (winner) status = … — write the expression form, or useMemo). Two positions deliberately keep their mount-time snapshot, because that's React's own semantics: useState/useRef/useReducer initial values and useEffect(fn, []) bodies. And bare side effects in the body still run exactly once — put reactions in useEffect.
Upgrading from an earlier Kog: pure body derivations used to freeze silently at their mount-time value; they are now live. If code actually wanted the mount snapshot, say so explicitly — useState(() => expr)[0] — and it reads as intent instead of an accident.
4. Dependency arrays are honored but can't cause staleness
useEffect(fn, [a, b]) re-runs the effect when a or b change, as in React. But a missing dependency never gives you a stale value (see #1) — so the exhaustive-deps lint is a warning about clarity, not a bug alarm.
5. No re-renders, no StrictMode double-invoke
Effects run once at mount, cleanup once at unmount. There is no development-mode double invocation, because the failure mode it guards against (impure renders) doesn't exist.
6. Context updates are fine-grained
Consuming a context that changes doesn't re-run your component (nothing does). Only the expressions that read the context value update.
Two structural rules
The compiler needs your JSX to be statically analyzable in two places:
- Early returns must be guards.
if (loading) return <Spinner/>is fine at the top of the component, before other logic. A reactive early return buried mid-function is a compile error — the error message will point you to a ternary or explicit conditional rendering. Textchildren are text.<Text>accepts strings, numbers, and expressions — not nested elements (LVGL labels are leaf widgets). The linter flags it.
Both are enforced by @kog/eslint-plugin (preconfigured in the template) with messages that show the fix.
What you never think about
- When to memoize — never for performance. A bare
constderived from state is kept live by the compiler (divergence #3);useMemois the same derivation spelled explicitly, and the compiler's suggested fix when an initializer isn't provably pure.useCallbackis free (identity is stable by construction).React.memohas no meaning; there's nothing to skip. - Render performance — there are no renders. A slider updating 60×/second executes one native setter 60×/second, regardless of how big your component tree is.
- Keys for static lists —
.map()over changing data does want keys (the linter reminds you); everything else just mounts once.