[Guide] Build a "One Page App" (Still Use a Router) - implementation notes
The One Page App guide is a useful reminder that "single screen" doesn't mean "no routing". The guide treats the router as an organization tool and future-proofing contract: file route for the shell, component routes for internal motion, and global state only where posture truly needs to be global.
If the UI is truly one page, what did you use routes for: internal panels, search state, or just future expansion? How did you split file router vs component router responsibilities without duplicating state? What global state did you allow, and how did you keep it from becoming a dumping ground? Did you render route evidence (active panel, active tab) so screenshots are actionable?
Comments (24)
Back to latestWe used routes for panels + deep links, even though the UI is one page.
It made support and QA way easier.
The router was mostly for naming posture: panel=editor, panel=preview, tab=settings.
Once posture is explicit, you stop passing flags everywhere.
We followed the guide pretty closely: file route owns chrome, component routes own internal motion. The biggest win was that we could log motion as routing decisions:
txt
[tips] routeFile shell=app reason=boot
[tips] componentRoute enter panel=editor reason=deepLink
[tips] componentRoute enter panel=preview reason=userClickDocs tie-in: the "router as contract" framing fits the routing docs well.
Related: Routing and Navigation and Getting Started.
Concrete alternative: no router, just local state.
We tried that first. The moment we needed shareable links and back/forward to work, we re-invented routing anyway. The guide is right that you should just start with a router.
Exactly. The real question is whether you want to invent a private router or adopt a public one.
Even a tiny one page app benefits from having a named motion model.
We allowed global state only for user posture (theme, account, feature flags). Everything else was route state or local state.
If a state value affects navigation, it should probably be route state so you can deep link it.
The guide's best advice is "leave an extensions path". We created fake routes (disabled) in the file tree so future sections had a place to land.
It sounds like busywork, but it prevented a painful reorg later.
Long-form: people treat routers like infrastructure, but in practice they're product vocabulary.
A one page app still has product states: which panel, which selection, which mode. If those are implicit, your app becomes difficult to support and difficult to test.
By making them routes (file or component), you get identity, evidence, and replay. That makes debugging feel like reading a script rather than guessing.
We rendered data-panel, data-tab, data-mode on the shell and it made screenshots genuinely useful.
Without that, bug reports were basically "it didn't work".
Small disagreement: I don't love component routes for tiny apps.
But I do love file route as the shell and route state as the one place for shareable posture. That alone solves 80%.
We implemented a very small component router wrapper with a strict contract (only accepts an enum + evidence).
The constraint prevented people from smuggling in arbitrary navigation strings and kept it maintainable.
Hot take: the router is worth it just for back/forward support.
If you want the guide's approach to feel real, add a route-flow test that navigates through panels and asserts evidence keys.
It makes refactors safe.
We used router state for search query too, because it made sharing links straightforward.
We still kept a local draft so typing didn't spam navigation. Commit/draft split was important.
Long-form counterpoint: there is a risk of over-architecting a one page app.
The guide can be misread as "always build a full router system". I think the real advice is "name motion and posture". If you can name it without building a big abstraction, do that.
We used a single file route plus a component route map for internal panels. It stayed stable as we added features.
The biggest win was that teams could add panels without touching global state.
The guide's approach also makes analytics easier because routes give you stable event names.
If the app is one page with 40 states, you need those names somewhere.
Takeaway: don't confuse one page with one state bucket.
Use routing (file or component) to keep state surfaces honest.
If you're planning to add anything later, start with the router now.
Otherwise you'll be migrating ad-hoc booleans into a route model under deadline.
One thing we did differently: global state for selection, route state for panel.
Selection is often not share-worthy; panel is.
Evidence suggestion: print a compact route string on the footer in dev builds.
It's like a built-in reproduction script.
This guide convinced me to stop being allergic to routers in small apps.
A practical note: if you do route state for internal panels, make sure you normalize/validate it.
We had a bug where an old link pointed to an invalid panel, and without normalization the UI crashed.
Long-form: the still use a router message is really about maintaining a stable contract as teams grow.
Once you have multiple people shipping changes, you want motion to be a shared language, not an emergent property.