Skip to content

Recenter router around Suspense transitions#10

Open
KidkArolis wants to merge 17 commits intomasterfrom
use-query
Open

Recenter router around Suspense transitions#10
KidkArolis wants to merge 17 commits intomasterfrom
use-query

Conversation

@KidkArolis
Copy link
Copy Markdown
Contributor

Summary

  • move router docs and migration guidance to the 0.7.0 Suspense-transition API
  • fix route path-param injection so route segments receive their own params as props
  • tighten the loading modes demo, including shorter demo delays and Mode D detail fade behavior

Verification

  • npm test
  • npm run demo:build

KidkArolis and others added 17 commits April 28, 2026 09:40
Why: the 0.6.x escape hatches (onNavigating, onNavigated, useRoute
injection prop) predate React's useTransition and force route state
out of the router, which breaks Suspense's transition contract — the
commit needs to be inside startTransition for the previous route to
stay on screen while the next one prepares.

Router: drops onNavigating/onNavigated/useRoute props. State is now
internal (useState + useTransition). Adds:

- prepare(ctx) per route — returns PreparedHandle[] for figbird-style
  fetch-as-you-render data loading. Router pins handles for the
  lifetime of the committed nav and releases them on the next commit
  or on Routes unmount.
- resolver: () => import('./Page') — preloaded at navigate time and
  rendered via React.lazy.
- transformRoute() — synchronous pre-commit URL rewrite hook,
  replaces the one legitimate use case for onNavigating
  (e.g. persisted-query restoration with history.replaceState).
- usePending() — exposes useTransition's isPending for top-bar
  progress and "click did something" affordances.
- DelayedSuspense + Router pendingDelayMs — encapsulates the "hold
  previous route for N ms then degrade to skeleton" pattern.
  Internally uses a never-resolving-promise fallback during the
  hold window so suspension propagates to the outer transition,
  then swaps to the real fallback once the threshold elapses or
  the boundary post-commits with reads still pending.
- defineRoute / defineRoutes — typed identity helpers.

Tests: rewritten for the new API; new coverage for transformRoute,
usePending, and the prepare/release lifecycle.

Docs: README, docs/content/_index.md fully updated. New MIGRATION.md
walks 0.6.x → 1.0 with recipes for the removed surfaces.

Demo: examples/loading-modes/ — Vite app showcasing the three
loading-mode patterns (immediate+skeletons, wait-for-ready, timed
fallback) against simulated chunk + data latencies, so the API
choices can be felt against real timings rather than argued in
the abstract.

dist/ rebuilt.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Plain object routes were always supposed to be the canonical shape;
the `defineRoute` / `defineRoutes` helpers in 1.0-WIP were leaning
toward a typed-routes future that doesn't fit how this library is
actually used (humaans has 100+ stable, simple-string param routes;
the wrapper-per-route ceremony wasn't earning its keep against
component-level typing).

Two changes that together replace the helpers:

- The `<Routes>` renderer now spreads each matched path param onto the
  leaf segment's component as own props. Each segment receives only
  the params declared in its own `path` — wrapping layouts that
  didn't declare those params get nothing extra. Static `props` from
  the route definition still spread alongside and win on key
  collision so consumers can override intentionally.

- `defineRoute` and `defineRoutes` are removed. Routes are plain
  objects in plain arrays. Components type the params they expect
  via their own function signature; the runtime injection meets them
  at that boundary. `prepare(ctx)` keeps `ctx.params` typed as
  `Record<string, string>` — typo-resistance via TypeScript wasn't
  worth the per-route wrapper or the mapped-tuple helper alternative.

Net result: humaans-style routes stay as `{ path, resolver, prepare }`
plain objects, and a page like:

  export default function Workflow({ workflowId }: { workflowId: string }) { ... }

receives `workflowId` for free from a `path: '/workflows/:workflowId'`
route — no `useRoute()` dance, no helper wrappers, no codegen.

Demo's ModeD migrated to declare `{ id?: string }` directly instead
of reaching for `useRoute()`. Tests, MIGRATION, and docs updated.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
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.

1 participant