Skip to content

refactor: share the rendering core and prop types of Router and RouteDisplay - #273

Merged
uhyo merged 2 commits into
masterfrom
claude/refactor-interception-route-tree
Oct 22, 2026
Merged

uhyo merged 2 commits into
masterfrom
claude/refactor-interception-route-tree

Conversation

@uhyo

@uhyo uhyo commented Oct 22, 2026

Copy link
Copy Markdown
Owner

Summary

Follow-up to #270: step 2 of its refactoring plan (deduplication), plus shared prop types. No change in behavior or public exports.

Step 1 of the plan (moving interception out of NavigationAPIAdapter) was tried and dropped. The interceptor still had to share the adapter's state, so the coupling stayed: an instanceof check plus six internal methods on the adapter. All of that saved about 1.3 KB in a RouteDisplay-only bundle.

Shared rendering core (Router/RouteTree.tsx)

The new internal RouteTree component holds what was duplicated between Router and RouteDisplay:

  • resolving feature flags and the browser() check
  • the SSR URL from ssr.path
  • deciding whether loaders run
  • loader cache keying (the entry key, or a per-instance cache when there is no entry)
  • route matching and running loaders
  • the RouterContext value and the providers around RouteRenderer

After this:

  • Router = createAdapter(fallback) + useLocationEntry + adapter.setupInterception + RouteTree
  • RouteDisplay = createDisplayAdapter() + useLocationEntry + RouteTree

Shared prop types (Router/props.ts)

  • RouteTreeSharedProps defines routes, ssr, trailingSlash and features once, each with a single doc comment. SSRConfig and RouterFeatures moved to the same file; Router/index.tsx re-exports them, so the public exports are unchanged.
  • RouterProps, RouteDisplayProps and RouteTreeProps are interfaces that extend it, with no intersection types.
  • Router passes the shared props it doesn't use itself straight to RouteTree (...sharedProps), and RouteDisplay passes all its props. A new shared prop can't be forgotten in one of them.

Changes users can see in types and docs:

  • Doc comments: routes gets a doc comment. The ssr comment now says "the real URL of the page takes over" so it fits both components.
  • Interfaces instead of type aliases: RouterProps and RouteDisplayProps are now interfaces. Unlike the old aliases, they can't be assigned to an index-signature type such as Record<string, unknown>.
  • Internal name in .d.ts: the published types show RouterProps extends RouteTreeSharedProps. The base interface is emitted but not exported.

Docs

  • The <RouteDisplay> section of the Components API page now says it assumes nothing else on the page intercepts navigations. If something else does, entry changes still render, but loaders don't re-run on an intercepted reload and the navigation info isn't exposed.
  • The Component Rendering section of CLAUDE.md now describes RouteTree.

Bundle size

Minified with esbuild against dist/, React excluded: about 12.4 KB for an entry importing only RouteDisplay and 13.0 KB for one importing only Router. Both are about 200 B larger than on master because of the extra component.

Test plan

  • pnpm test:run: 371 tests pass, with no test changes
  • pnpm typecheck, pnpm lint, pnpm format:check, pnpm --filter @funstack/router build
  • Docs site build: not run locally, because the container has Node 22, which has no global URLPattern. CI uses Node 24.

Generated by Claude Code

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
Define the props both components pass to RouteTree (routes, ssr,
trailingSlash, features) once, as RouteTreeSharedProps in
Router/props.ts, together with SSRConfig and RouterFeatures. RouterProps,
RouteDisplayProps and RouteTreeProps extend it as interfaces, and Router
and RouteDisplay forward the shared props to RouteTree as a whole, so a
new shared prop cannot be missed in one of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_016vNbiZGrzNhyfyctwm4NbJ
@uhyo
uhyo merged commit 9bbfd24 into master Oct 22, 2026
1 check passed
@uhyo
uhyo deleted the claude/refactor-interception-route-tree branch October 22, 2026 07:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants