• Switch to Warm
  • Switch to Cool
  • Workspace
  • Projects
  • Notes

Retrospective — Learning Astro and Svelte, Coming From C++/Qt

Retrospective

Published Jul 23, 2026

Part of: Workspace

Islands, coming from Qt

The first thing that took adjustment was how little of the site is actually “running” at any given moment. Most of a page is plain HTML with zero JavaScript attached — the Hub, a project narrative, a note body, none of that ships a runtime unless something on the page genuinely needs one. Only specific components hydrate into live, interactive code, and only because a directive says so: NavBar.astro mounts CommandPalette, Switcher, and ThemeControl each with client:load; SearchIsland and NotesPageIsland do the same on the Notes page. Nothing hydrates by default — you opt a component in, explicitly, every time.

Coming from Qt, there’s no equivalent notion to reach for. A Qt application is one process, and every widget in it is alive from the moment the event loop starts — a QPushButton doesn’t have a dormant, inert state waiting to be told to wake up; it’s already a fully constructed object with live signal/slot connections before the window is even shown. Astro’s islands architecture assumes the opposite default: inert unless asked, and most of the page stays that way permanently. It’s a model built around a question Qt never has to ask, because Qt has no “static HTML” to fall back to in the first place.

Reactivity without signals and slots

This project runs Svelte 5 (^5.56.6), but the components in it — ThemeControl.svelte is a clear example — use the older reactivity style: export let for props, plain let bindings for local state, and $: expanded = hovered || focused || locked; for derived values, not the newer $state/$derived runes. So the mental model I actually had to build was: reassigning a variable is what the compiler treats as a signal to re-render, not a runes-based dependency graph.

That’s a different shape of thinking than Qt’s signal/slot system. In Qt, reactivity is explicit and directional — you connect a signal to a slot, and something fires because a specific emit happened. In this Svelte style, there’s no connect() to write and no event to name; you just assign to current or locked like an ordinary variable, and the compiler has already instrumented that assignment at build time to know it needs to patch the DOM. My first instinct kept reaching for something to subscribe to. There wasn’t one — the assignment is the notification.

The seam I didn’t expect: route.ts

The genuinely tricky part was src/stores/route.ts, and it only made sense once I ran into the problem it exists to solve. The Switcher island — the nav bar’s active-destination indicator — is mounted with transition:persist="nav-switcher". That directive tells Astro’s view transitions to keep the exact DOM node (and the component instance inside it) across a client-side navigation, instead of throwing it away and re-rendering it fresh. Great for not re-mounting the nav bar on every page — but it means that component’s props are frozen at whatever they were on first paint, forever, for the life of the session. It never gets told the path changed, because from Astro’s point of view, that subtree was never re-rendered.

So the active-route highlight would just be wrong after the first navigation, and nothing about the component itself was broken — the props it needed simply never arrive a second time. The fix is a tiny nanostores atom, currentPath, that the persisted Switcher subscribes to directly, updated on the browser’s astro:page-load event rather than through props. It’s a narrow, purpose-built escape hatch: a normal, non-persisted island doesn’t need this at all, because Astro re-renders those with correct server props every time. The “aha” wasn’t nanostores itself — it was realizing that transition:persist quietly creates components that Astro’s rendering model can no longer talk to, and that anything living inside one needs its own private channel if it cares about later navigations at all.

What’s still fuzzy

Not everything clicked cleanly. I still don’t have a precise mental model of exactly which islands get torn down and rebuilt versus reused as-is across a view transition when transition:persist isn’t involved — I know the persisted case cold now because I had to debug it, but the default case I’m mostly trusting rather than fully understanding. And the nanostores seam here is a single, narrow instance; I haven’t yet had to reach for the same pattern anywhere else, so I genuinely don’t know yet whether it generalizes cleanly the next time something persisted needs live state, or whether this one just happened to be simple.