Repository navigation
fix(router): swap app shell on cross-router SPA navigation - #476
Merged
Merged
Conversation
Previously, when a same-origin SPA navigation crossed into a different
router (via host router path mount), the client detected the routerId
mismatch and downgraded to a full tree rebuild — but the target app's
rootLayout, basename, and version all stayed captured from the initial
load. The target app's Document was never rendered.
Introduces a live AppShell ref holding { routerId, rootLayout, basename,
version }. renderSegments reads rootLayout through the ref at call time
so the initial closure capture is gone; NavigationProvider exposes
basename/version as live getters on a stable context value so consumers
read current values without memo-invalidation churn.
On routerId mismatch, partial-update invokes bridge.applyAppShell(next)
before the full-update render. applyAppShell updates the ref, sets the
app version, rebinds rango-state to the target app's namespaced
localStorage key, and drops this tab's navigation + prefetch caches
locally — no cross-tab broadcast, no shared-state rotation.
Contract decisions:
- theme, warmup, and prefetchCacheTTL are document-lifetime. ThemeProvider
sits above the segment tree and must not remount on smooth transitions;
warmup toggles tear down long-lived idle listeners; prefetchCacheTTL is
not serialized on every full-render path (e.g. the not-found fallback),
so carrying it in the mutable shell would silently erase it on cross-
app nav to a 404. A full document navigation applies these.
- rango-state is namespaced per-router (rango-state:{routerId}) so sibling
apps on the same origin cannot observe each other's invalidations. The
storage listener filters by the current key so smooth transitions in
this tab don't let other tabs in the old app clobber the B-app state.
- Smooth app-switch does not write the shared rango-state key or broadcast
cache invalidation. setRangoStateLocal mutates only this tab's in-memory
state; clearHistoryCacheLocal skips broadcast.
E2E coverage in examples/cloudflare-multi-router covers dev + production:
- cross-app SPA nav swaps rootLayout (data-app-shell attribute in each
app's Document changes from a→b without full reload)
- back navigation restores previous rootLayout
- cross-tab app isolation: tab 1 smooth-switches A→B while tab 2 invalidates
A; tab 1 must keep sending B's rango-state on its next request
Verified by temporarily relaxing the storage-key filter — the isolation
test correctly flags the cross-app token bleed.
commit: |
ivogt
marked this pull request as ready for review
April 21, 2026 12:11
rango-state's localStorage key is now `rango-state:{routerId}` for
multi-app isolation; legacy `rango-state` remains the fallback when no
routerId is supplied. Add a helper that locates the key regardless of
namespace and replace direct `localStorage.getItem("rango-state")` reads
in tests/vite-rsc-demo.
Also picks up format fixes on README.md and skills/links/SKILL.md that
oxfmt 0.35 flagged after the main merge.
ivogt
marked this pull request as draft
April 21, 2026 15:16
…rective
oxfmt treated mixed server+client snippets as one TS module and
rewrote the mid-block "use client"; as ("use client");, turning the
directive into an ineffective parenthesized string expression. Users
copying those examples would get server components by mistake.
Split each mixed example into two adjacent fenced blocks so the
client side starts with "use client" at the top — oxfmt now
recognizes it as a valid directive prologue and leaves it bare.
Affected: README.md and skills/{links,hooks,typesafety,breadcrumbs}
/SKILL.md.
# Conflicts: # packages/rangojs-router/skills/links/SKILL.md
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Cross-router SPA navigation now swaps the target app's document shell instead of reusing the initial app's. Previously, the client detected the routerId mismatch, downgraded to a full tree rebuild, but kept the initial app's
rootLayout,basename, andversioncaptured in closures/memos — so app B's Document never rendered.Changes
AppShellRefholds{ routerId, rootLayout, basename, version }.renderSegmentsreadsrootLayoutthrough the ref at call time;NavigationProviderexposesbasename/versionas live getters on a stable context value (no memo churn).applyAppShellon mismatch: inpartial-update.ts, when routerId changes, invokes the newbridge.applyAppShell(next)before the full-update render. It updates the ref, rebindsrango-stateto the target app's namespaced localStorage key, and drops this tab's nav + prefetch caches locally (no cross-tab broadcast, no shared-state rotation).rango-state:{routerId}instead of a single key. Sibling apps on the same origin can't observe each other's invalidations; the storage listener filters by the tab's current key so smooth transitions don't let other tabs clobber this tab's state.themeConfig,initialTheme,warmupEnabled, andprefetchCacheTTLare intentionally NOT part of the shell — each has a documented reason (ThemeProvider must not remount; warmup idle listeners; TTL not serialized on every full-render path). A hard document navigation applies them.Test plan
examples/cloudflare-multi-router(dev + production):rootLayout(observable via per-Documentdata-app-shellattribute) without full reload.rootLayout.rango-state-invalidation.test.tsandcross-tab-invalidation.test.tsto locate the namespaced key via a helper (instead of the legacy single-app key).Out of scope (follow-ups)
clearHistoryCacheLocalon app switch covers the common case today).