Troubleshooting

AFW components run once at connect (init-once), not on every state change. Most "my UI didn't update" reports trace back to that model, signal reassignment rules, or server/client render drift. This page collects the frequent cases and where to read more.

The UI does not update when state changes

Put conditionals inside JSX holes

if (cond) return <A /> and early-return guards are evaluated once at connect. They are not reactive regions — flipping a $state later does not swap the branch.

JSX
// ❌ frozen at first connect
if (!user) return <Login />;
return <Main user={user} />;

// ✅ reactive — inline hole or single return with JSX branches
return user ? <Main user={user} /> : <Login />;
return <div>{user ? <Main user={user} /> : <Login />}</div>;

Full walkthrough: Templating → Init-once and JSX → Unsupported patterns.

Do not cache JSX in a local variable

JSX assigned to const view = … is built once. Embedding {view} later is a plain value hole, not a reactive switch:

JSX
// ❌ branch fixed at connect
const panel = user ? <Main /> : <Login />;
return <div>{panel}</div>;

// ✅ conditional inside the returned JSX
return <div>{user ? <Main /> : <Login />}</div>;

Reassign $state, do not mutate it

Signals notify on reassignment, not in-place mutation. The compiler rejects common mutators (push, splice, property assignment, nested ++) on $state values with an error that points at the immutable rewrite.

JSX
// ✅
todos = [...todos, item];
todos = todos.filter((t) => t.id !== id);

// ❌ compile error or silent no-op
todos.push(item);
todos[0].done = true;

When nested objects need direct mutation, use reactive() instead — see Reactivity.

Compile errors

Error / patternFix
ref={(el) => …} callback refUse $ref() + onMount / $effect / $expose
<Foo.Bar /> member componentImport Bar and render <Bar />
$state mutation (push, x.y = …, nested ++)Reassign a new value, or switch to reactive()
{ [key]: val } computed prop keysUse static identifier keys in JSX attributes
Lifecycle hook inside a helper / if / after awaitMove onMount, onCleanup, etc. to top-level statements in the component body

Callback refs and member components are called out in the JSX reference.

Hydration mismatches

With SSG or SSR, the client adopts the server HTML on first paint. If structure diverges, the runtime reports a hydration mismatch through the runtime error API and rebuilds that component via CSR. The rest of the page stays adopted.

Common causes:

  • Non-deterministic render — Date.now(), Math.random(), or reading window / localStorage during render. Gate client-only UI with onMount or a post-hydration signal.

  • Loader vs client fetch — pre-rendered HTML should match what the client paints before its own fetch completes. Use route loaders so SSG/SSR inline the same data the client reads on hydration.

  • Different branches server vs client — avoid user ? <Dashboard /> : <Login /> when user is only known in the browser unless the server emits the same branch the client will adopt first.

Details: Hydration.

Fragment route roots

Multi-root fragment outputs (a page that returns <>…</> without a wrapper) are a known gap on the adopt path — the subtree may fall back to CSR. Wrap the page in a single element when you need reliable hydration.

Lifecycle hooks on pages

onResize and onVisibilityChange observe the host element. A page using them needs a single element root — not a bare fragment. The compiler warns when the root is a fragment.

onMediaQuery has no element constraint. All lifecycle macros must be top-level statements. Calls inside helpers or conditions are unsupported; component bodies must remain synchronous. During SSG/SSR they do not run at all — only in the client bundle.

See Lifecycle.

Compiler diagnostics (esdev start)

The compiler plugin returns file, line and column diagnostics to esdev. With the default project plugin, compile errors fail the build. Fix the reported location and wait for a successful rebuild.

The compiler emits source maps for JSX/TSX transforms, which esdev can compose with the bundle's map. Enable build source maps when you need to debug release output. Markdown and MDX first become generated JSX, so their diagnostics and maps can still point into that generated source rather than the original prose.

Runtime errors go through the framework's error-reporting API, including the afw:error event and console logging. <ErrorBoundary> can show a fallback while still reporting the error. The native compiler plugin does not supply the retired CLI's /__route stack rewriting or error-overlay controls.

See Error boundaries.

SEO and metadata

  • Per-route titles and share cards need SSG or SSR — each URL must ship its own <title>, canonical, and generateMetadata output. A plain CSR build serves one shared index.html for every route, with the head provided by that HTML entry. See Metadata & SEO.

  • SPA navigation never updates <head> — after first paint, <Link> navigations do not re-run metadata. Crawlers and social previews use the document for the URL they fetched, not the client-rendered view.

  • Missing absolute URLs — set site.url in afw.config.js for your prerender script so canonicals, og:image, and the sitemap are absolute.

  • Dynamic routes not in the sitemap — export getStaticPaths when pre-rendering with a configured prerender target; otherwise those paths have no static HTML (but a request renderer / SSR still resolves generateMetadata per request).

Quick decision tree

TEXT
UI stuck after setState?
  ├─ conditional outside {…} in JSX?     → move into a JSX hole
  ├─ JSX stored in const/let?            → inline the conditional
  └─ mutating array/object in place?     → reassign or use reactive()

Compile failed?
  ├─ callback ref / <Foo.Bar />          → $ref / direct import
  └─ $state.push or x.y = …             → immutable reassignment

Flash or hydration warning on load?
  ├─ Date.now / window during render?    → onMount or loader data
  └─ fragment page root + observers?     → wrap in one element

Wrong title in Slack/Google?
  ├─ plain CSR only?                     → configure prerendering or a request renderer for per-route <head>
  └─ deep link works but in-app nav wrong? → expected: <head> is not updated on <Link>
Last updated on
Edit this page