Thinking in Kog
Kog looks like React Native because it is React Native at the surface: TSX, hooks, View/Text/StyleSheet from 'react-native', the component mental model. But the engine underneath uses fine-grained reactivity instead of a virtual DOM. Your component function runs once, at mount; 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's engine.
Two promises
Kog holds every part of itself to two rules:
- React Native is the reference. Component names, props, defaults and APIs follow React Native. Where Kog can't match it, because the display library can't draw something or a microcontroller can't afford it, the difference is written down in Coming from React Native with its reason. There is no CSS, no
classNameand no HTML; each is a compile error that names the React Native replacement. - The engine doesn't defy what you know about JavaScript. A
Maplookup or delete is O(1), array indexing is O(1), and iteration is live, the same as in a browser. Anything that can't work fails loudly with a message that tells you the fix, instead of silently doing nothing.
The differences on this page are the allowed kind: they change how your program runs, not what a correct program computes.
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 most common slip from useState habits 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 once, at mount. It is not "the render function": there is no render loop to re-run it. Derived values stay fresh because the compiler analyzes your derivations. A binding is kept live automatically when it is computed purely from state, props, a @kog/router location hook (const { pathname } = useLocation()), or another derived binding, and something reactive reads it. 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.
When the compiler cannot safely keep a binding live, it fails the build with an error that names the binding, instead of letting it freeze silently. Two kinds of binding trigger this: 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 use useMemo). Two positions intentionally 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 your code wanted the mount snapshot, say so explicitly with useState(() => expr)[0], and it reads as intent instead of an accident.
4. Effects can't see stale values, and an effect without deps tracks itself
useEffect(fn, [a, b]) follows React: it re-runs only when a or b changes (Object.is), and [] means mount-only. What differs is staleness. State reads inside the effect always return the current value, so a dependency you forgot to list never hands you an old one; it only means a change to it doesn't re-run the effect. Leave the array off and the effect is auto-tracked: it re-runs when any reactive value it reads changes. The exhaustive-deps lint is a warning about when the effect re-runs, not about stale reads. useMemo's deps array is still ignored, because the memo tracks its own reads.
The same goes for an effect's cleanup: it reads state live, so when the effect re-runs, the cleanup sees the new value, where React's cleanup sees the value from the render that created it. If the cleanup needs the old value, copy it into a const inside the effect.
5. No re-renders, no StrictMode double-invoke
Nothing re-runs a component body, and nothing double-invokes effects: a mount-only effect runs once at mount and its 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 doesn't re-run your component (nothing does). A provider's value is read once at mount, so for a context that changes, provide an accessor (value={() => theme}) and call it where you read it. Then 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 <ActivityIndicator/>andif (!visible) return nullare fine at the top of the component, before other logic, and they stay reactive: the component shows or hides as the condition changes. An early return buried mid-function is a compile error; the error message points you to a ternary or a top-level guard. Textchildren are text.<Text>accepts strings, numbers, and expressions, but not nested elements (LVGL labels are leaf widgets). The compiler rejects them. Values render as in React:false,true,null, andundefinedshow nothing, so{count > 0 && count}is empty at zero.
Both are enforced by the compiler, with messages that show the fix. @kog/eslint-plugin (preconfigured in the template) also flags reactive early returns as you type.
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 (without one, rows are keyed by index); everything else mounts once.