Repository navigation
feat: add RouteDisplay component for route rendering without navigation interception - #270
Merged
Merged
Conversation
…on interception StaticRouter matches routes against the current URL and renders them, but never subscribes to or intercepts navigations. This makes it usable for MPAs where each page uses route definitions to decide what to show, with SSR support via the same `ssr` prop as Router. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoUA6HQioyuW5Y6iX8wvZZ
…ads, expose entry details - Rename StaticRouter to RouteDisplay. - Navigations requested by hooks (useSearchParams setter, route setState) are performed as regular full page navigations instead of warning: navigation.navigate() without interception (state carried over to the new entry), or location.assign/replace without Navigation API. - When Navigation API is available, expose the current entry's id, key and state, and follow currententrychange so setStateSync works. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoUA6HQioyuW5Y6iX8wvZZ
The top-level React export lookup was retained by bundlers even when Router was unused (e.g. apps that only use RouteDisplay), since property access can't be proven side-effect-free. Resolve it in a pure-annotated call instead. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoUA6HQioyuW5Y6iX8wvZZ
Extract Router's location tracking (adapter subscription, hydration handling, entry-change transitions) into a shared useLocationEntry hook and use it in RouteDisplay together with the same adapters Router uses, without setting up interception. - RouteDisplay uses NavigationAPIAdapter when available, otherwise a StaticAdapter with the new fullPageNavigation option. - Loader cache keying matches Router (entry keys, dispose cleanup, carry-over across same-URL replaces). - Same-document entry changes render in a transition, so useIsPending reflects them. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AoUA6HQioyuW5Y6iX8wvZZ
uhyo
pushed a commit
that referenced
this pull request
Oct 22, 2026
Follow-up to #270, with no behavior change. Extract the rendering logic duplicated between Router and RouteDisplay (feature flags, SSR URL, loader execution and cache keying, RouterContext and providers) into Router/RouteTree.tsx. Router and RouteDisplay now only differ in how they create the adapter and whether they set up interception. Also document that RouteDisplay assumes nothing else on the page intercepts navigations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016vNbiZGrzNhyfyctwm4NbJ
2 of 3 tasks
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
Adds
<RouteDisplay>, a component that only decides what to render from the current URL. It does not intercept navigations. It is meant for multi-page applications (MPAs) where each page is a separate document but still uses FUNSTACK Router route definitions to decide what to show, including SSR.Behavior
routes,ssr,trailingSlash,features, all working the same as on<Router>.Router:Router's location tracking (subscribing to the adapter, the hydration handling, and the transitions on entry changes) is moved into a shared internal hook,useLocationEntry.RouteDisplayuses that hook with the same adapters and simply never callssetupInterception:NavigationAPIAdapter, so the entry id, key and state are exposed, and same-document entry changes (e.g. hash navigations,setStateSync) render in a transition;StaticAdapterwith a newfullPageNavigationoption.Router(the entry's key, cleanup when an entry is disposed, results carried over across same-URL replaces). When there is no entry (SSR), the cache is scoped to the instance.useSearchParamssetter, routesetState/resetState) do a full page navigation:navigation.navigate()with no interception, andstateis carried over to the new entry;location.assign()/location.replace().useBlockernever blocks, and route actions are not run (forms submit to the server).Routerunchanged: its behavior and itsfallback="static"warning stay the same. The hook extraction moves code without changing what it does.Tree shaking
RouterdropsRouteDisplay.RouteDisplaydropsNullAdapter, but currently keepsNavigationAPIAdapter's interception and form-action methods, because bundlers can't remove individual class methods (≈12.2 KB vs ≈12.9 KB minified forRouter, React excluded). Step 1 of the follow-up plan below fixes this.core/addTransitionType.tsnow resolves React's export through a/* @__PURE__ */call.Docs
<RouteDisplay>section to the Components API page.fallback="static"section of the SSR guide.Follow-up refactoring plan (separate PR)
Neither step changes the public API, so both can land after this PR without user-visible changes. They are independent of each other and can be one PR or two.
1. Split interception out of
NavigationAPIAdapter(tree shaking)Goal: a
RouteDisplay-only bundle no longer contains the interception code, bringing it back to about 7–8 KB minified, roughly what it was before this PR adopted the adapters.NavigationAPIAdapter: observing the location:getSnapshot,subscribe(including the WebKit Private Browsing fallback), entrydisposecache cleanup, carrying cache over across same-URL replaces,navigate/navigateAsync,updateCurrentEntryState,getIdleAbortSignal.navigateevent handler (currentlysetupInterception, plusfindActionRouteand the form-action / revalidation flow) moves to a new module, e.g.core/navigationInterception.ts, assetupNavigationInterception(adapter, getRoutes, onNavigate, checkBlockers, getMatchOptions). OnlyRouterimports it.#currentNavigationInfo,#lastInterceptedNavigationType,#reloadCounts,#pendingFormNavigationand#committedDestination, and invalidates#cachedSnapshot. These become a small internal API on the adapter that interception calls, for example:recordInterceptedNavigation({ info, navigationType })bumpReloadCount(key)beginFormNavigation()/endFormNavigation(handle)setCommittedDestination(entryId, href)setupInterceptionfrom theRouterAdapterinterface, and with it the no-op implementations inStaticAdapterandNullAdapter.RoutercallssetupNavigationInterceptiononly when its adapter is aNavigationAPIAdapter.2. Share the rendering core between
RouterandRouteDisplay(deduplication)Goal: route matching, loader and SSR behavior live in one place. Today about 100 lines are duplicated between
Router/index.tsxandRouteDisplay/index.tsx.RouteTree. It takes the resolved location (locationEntry,actualLocationEntry,isPending), the adapter (fornavigateAsync,updateCurrentEntryState,getIdleAbortSignal),routes,ssr,trailingSlash,featuresand the blocker registry. It owns:browser()checkssr.pathRouterContextvalue and the provider tree aroundRouteRendererRouter=createAdapter(fallback)+useLocationEntry(adapter, transitionTypes)+ interception +RouteTreeRouteDisplay=createDisplayAdapter()+useLocationEntry(adapter)+RouteTreeVerification for the follow-up
Router, SSR loader-cache isolation, action revalidation, hydration, WebKit Private Browsing,RouteDisplay) pass without changes.dist/, one entry importing onlyRouteDisplayand one onlyRouter) confirms the interception code is gone from theRouteDisplay-only bundle.Test plan
RouteDisplay.test.tsx: rendering, nested routes, location and search params, loaders, full-page navigation from setters (with and without the Navigation API), entry id/key/state,setStateSync, a hash navigation rendered in a transition, loader results kept across a same-URLsetStatereplace, nonavigatelistener, SSR with and withoutssr/runLoaders, and hydration (including Navigation API state and reuse of loader results)Routertests pass unchanged after theuseLocationEntryextractionpnpm test:run: 371 tests passpnpm typecheck,pnpm lint,pnpm format:check,pnpm build🤖 Generated with Claude Code
https://claude.ai/code/session_01AoUA6HQioyuW5Y6iX8wvZZ