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." Side effects in the body don't re-run when state changes — put reactions in useEffect.
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 —
useCallbackis free (identity is stable by construction) anduseMemois a real derived value, not a render-time cache hint.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.