Guide

Troubleshooting & FAQ.

The questions developers hit most often, with the cause and the fix in one place. Most surprising behaviour comes from Aktion’s streaming-first defaults — once you know the rule, the fix is short.

Rendering & reactivity

My Input loses focus on every keystroke / state change

Cause. The morph reconciler preserves focus, selection, and input values across re-renders — but a subtree rendered through Portal is re-created rather than morphed, so a focused field inside a Portal loses focus whenever an unrelated state change re-renders the tree.

Fix. Keep focused, editable content out of a Portal. For overlays that must hold a form, prefer Modal (which traps and restores focus). If you must keep an input mounted, bind it to a top-level atom so typing only triggers a path-scoped update rather than a full re-render of the surrounding tree.

Typing in an Input doesn’t update my state (two-way binding)

Cause. A control only writes back when you pass the $variable itself as its bound argument. Passing $name’s value (e.g. interpolated into a string) gives the control a one-way snapshot — it renders, but has nothing to write to.

// ❌ no binding — the field shows the value but typing goes nowhere
Input({ placeholder: "Name", value: `${$name}` })

// ✅ pass the $variable last so the control reads AND writes it
$name = ""
Input("Name", { value: $name }, $name)

Fix. Pass the bare $variable as the last argument to Input, Select, Checkbox, Switch, Slider, MultiSelect, and the other bindable controls. See Components for each control’s binding argument.

My effect isn’t re-running

Cause. $effect(fn, [deps]) only re-runs when one of its tracked dependencies changes: reactive atoms / $store fields read in the dependency array, plus the component lifecycle and timer ticks. A value that isn’t a reactive read (a plain local, a prop captured by value) won’t re-trigger the effect when it changes.

// ❌ depends on a plain local — never re-fires when $userId changes
$effect(() => load(id), [])

// ✅ list the reactive atom; the effect re-runs when $userId changes
$effect(() => load($userId), [$userId])

Fix. Put the actual reactive atom(s) the effect depends on into the dependency array. If you need to react to a derived value, derive it from atoms (or a $memo) so the dependency is itself reactive. An empty array [] means “run once on mount”.

My component didn’t update — it was “memoized away”

Cause. On a fine-grained update, a PascalCase component whose props are unchanged is skipped and its previous DOM is reused. If the component reads state it didn’t receive as a prop and that state changes via a path the component never subscribed to, it can be skipped.

Fix. Read the reactive value inside the component (so it subscribes to the path), or pass it in as a prop so a changed value breaks prop equality and forces a re-render. Use DevTools’ “why did this render” to confirm whether an instance was skipped.

Data, lists & async

My list reorders, animates wrong, or loses per-row input state

Cause. Without a stable key, the morph reconciler matches list children by position. When the array reorders, inserts, or filters, DOM nodes (and their focus / input values) get reused for the wrong row.

// ❌ positional matching — inserting at the top shifts every row's DOM
$items.map(item => Card([Text(item.title)]))

// ✅ key by a stable id so identity follows the data
$items.map(item => Card([Text(item.title)], { key: item.id }))

Fix. Give every item in a mapped list a key tied to stable data identity (an id, not the array index). This also lets the memoizer skip unchanged rows — see Performance.

My $http data never appears (blank where the data should be)

Cause. An $http({...}) call returns a resource with a lifecycle (loadingdata / error), not the data directly. Rendering the resource object itself shows nothing useful while it’s still loading.

$users = $http({ url: "/api/users" })

// ✅ switch on the resource state with Async
Async($users, {
  loading: Spinner(),
  error:   Callout("Couldn't load users", { tone: "danger" }),
  empty:   EmptyState("No users yet"),
  data:    List($users.data.map(u => ListItem(u.name))),
})

Fix. Render the resource through Async(resource, { loading, error, empty, data }) (or read resource.data / resource.state yourself). See HTTP.

Common gotchas

Map(...) rendered something unexpected (not a JS Map)

Cause. Map is a built-in component name in Aktion (the geographic map). A bare Map(…) in render position resolves to that component, not the JavaScript Map constructor.

Fix. For a JS hash map use an object literal ({}) or the array/object helpers in $util ($util.keyBy, $util.groupBy). Reserve Map(…) for the map component.

My inline style was dropped

Cause. Styles pass through a sanitizer. Declarations that look unsafe (e.g. url(javascript:…), expression-based values) are dropped silently so streamed, partial output can never inject script.

Fix. Use plain, static CSS values. For anything dynamic, compute the value in an expression and pass a clean string. Turn on strict mode to get a console warning when a declaration is rejected instead of a silent drop.

My $theme({...}) override isn’t applying

Cause. Two things commonly bite: (1) the in-program $theme({...}) form takes grouped keys (colors.primary), while the host theme attribute and setTheme() take flat keys (colorPrimary); and (2) unknown token names are silently ignored as a typo guard, so a misspelled token does nothing.

// ✅ in-program: grouped keys
$theme({ colors: { primary: "#e11d48" }, radius: { md: "14px" } })

// ✅ host attribute: flat keys (JSON)
// <aktion-app theme='{"colorPrimary":"#e11d48"}'></aktion-app>

Fix. Match the key shape to where you set the theme, and check the token name against the Themes token reference.

A missing translation shows the key instead of text

Cause. When an $i18n key has no entry for the active locale, the runtime returns the key itself rather than throwing — so a partially-translated app still renders.

Fix. Add the missing key to the locale table. In strict mode, missing keys are surfaced as console warnings so you can find them during development.

Silent failures & warnings

Nothing rendered and there was no error

Cause. By design, unknown identifiers evaluate to null, non-callable callees return null, and an unknown component renders a Skeleton — so mid-stream partial output never crashes.

Fix. Enable strict mode (strict attribute on <aktion-app>) to turn these silent fallbacks into console warnings. See Error handling & debugging.

I got a “reactive write during render” warning

Cause. A $name = … assignment ran in render position (commonly a $x = … at the top of a lowercase function that is invoked to build UI). The runtime applies the write without scheduling a re-render to avoid an infinite loop, and warns.

Fix. Seed component-local state with a PascalCase component (so $x = … becomes a set-once per-instance declaration) or the $state hook, and only write state from event handlers / effects. See Reactivity & rendering.

Still stuck?

Two switches surface almost everything: add the strict attribute to <aktion-app> to turn silent fallbacks into console warnings (see Error handling), and open the DevTools panel to inspect live state and read each commit’s “why did this render” reason.

Next