Repository navigation
Custom cache keys for lazy route discovery #15374
brophdawg11
started this conversation in
Proposals
Replies: 0 comments
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Uh oh!
There was an error while loading. Please reload this page.
Summary
Add an optional client-side callback to
<HydratedRouter>that allows applications to customize how successful lazy route discovery requests are cached.After React Router successfully discovers
/product/a, it records the"product"cache key. If/product/bsubsequently matches the currently known/product/:idroute, React Router can use the cached key and avoid another/__manifestrequest.This is an opt-in assertion made by the application. Returning the same key for multiple pathnames asserts that discovering any one of those pathnames provides enough route manifest information to correctly match every pathname using that key.
Motivation
Lazy route discovery initially sends only the routes required for the current document. As users navigate, React Router requests additional route metadata from
/__manifest.Dynamic routes create an unavoidable ambiguity:
Once
/product/ahas been discovered, the client knows about/product/:id. When navigating to/product/b, that dynamic route matches locally, but React Router cannot assume it is the correct match. An undiscovered static route such as/product/bwould rank higher.React Router therefore performs another manifest request whenever the current match contains dynamic or splat parameters.
This is the correct default, but it can produce redundant requests for applications that know more about their route space. For example, an application may know that:
/product/:idhas no static exceptions;We want to let those applications suppress redundant requests without adding route analysis or additional CPU work to the publicly accessible
/__manifestendpoint.Proposed API
Example:
The callback receives:
pathname: The concrete pathname being discovered, such as/product/a.pattern: The currently matched, uninterpolated route pattern, such as/product/:id.Returning a string provides a custom discovery cache key. Returning
undefinedretains the existing exact-path caching behavior.The callback is synchronous and client-only.
Semantics
Successful discovery
Suppose the client does not yet know about
/product/:id./product/a./__manifest./product/:idto the client manifest./product/aand obtains the pattern/product/:id."product"."product"as discovered.Later:
/product/b./product/:idroute matches."product"."product"is already discovered, React Router skips the manifest request.Initial document matches
Routes included in the initial server-rendered manifest are authoritative for that document request. Their custom cache keys are seeded during hydration.
If the initial document is
/product/a, the application can navigate to/product/bwithout first making a client-side manifest request.Eager link discovery
The same cache applies to eager discovery from rendered links and forms.
After
"product"has been discovered, rendering this link does not make another manifest request:When the callback is not configured, eager discovery retains its existing behavior and does not perform additional route matching.
Failed or interrupted discovery
A custom key is recorded only after successful discovery.
The key is not recorded when:
Cache lifetime
Custom keys are held in the same in-memory, page-lifetime cache used for discovered pathnames. They are not persisted across document reloads.
Correctness contract
This API intentionally transfers part of the route-discovery correctness responsibility to the application.
Consider:
With this callback:
The following can occur:
/product/adiscovers/product/:id."product"is cached./product/blocally matches/product/:id./product/broute.The callback must account for known exceptions:
Returning
undefinedfor/product/bcauses it to use normal pathname-based discovery. Once the static route has been discovered, it will match as/product/brather than/product/:id.This is similar to APIs such as
shouldRevalidate: React Router provides a correct default, while applications may opt into a more aggressive optimization when they can guarantee its assumptions.Why include
pathname?A callback receiving only the matched pattern is simpler:
However, the known client route tree may produce
/product/:idfor both/product/aand an undiscovered static/product/b.Providing the concrete pathname lets applications preserve discovery for known exceptions:
It also supports applications whose grouping rules depend on tenant prefixes, locales, or other concrete pathname segments.
Server behavior
This proposal does not change
/__manifest.In particular, it does not:
The endpoint continues receiving concrete
pathsand returning their matched route manifest entries.All custom cache-key behavior happens on the client after the normal response has been applied.
Performance
The intended benefit is avoiding repeated network requests and server-side route matching for dynamic paths that the application knows are equivalent.
When the callback is absent, there should be no additional matching or callback overhead.
When configured:
/__manifestreceives no additional work.Scope
The initial proposal applies to conventional Framework Mode through
<HydratedRouter>.It does not initially apply to:
patchRoutesOnNavigation;unstable_RSCHydratedRouter.RSC Framework parity can be considered separately once the API semantics have been validated.
Alternatives considered
A
manifestGroupprop on linksThis is locally explicit but makes route-space knowledge a property of individual links. It would need to be repeated across the application and would not naturally cover programmatic navigation, redirects, forms, fetchers, or initial document loads.
It also cannot guarantee that the first group member loads all other routes belonging to that group.
A function in
react-router.config.tsFramework configuration is used during the build and on the server, while this optimization needs to execute in the browser. Arbitrary functions cannot be serialized safely into the client build without introducing an additional module or bundling contract.
Running the function on
/__manifestwould also add user-defined work to a public endpoint that is already performance-sensitive.Declarative build-time route groups
The compiler could precompute all route IDs belonging to each group and the endpoint could return the entire group on first access.
This can provide a stronger correctness guarantee, but it requires build-manifest changes, server protocol changes, client/server group synchronization, and potentially much larger manifest responses.
It also solves a different problem: eagerly loading a group instead of allowing applications to assert that the current manifest is already sufficient.
Automatically discovering competing routes
React Router could inspect flattened route branches and include every route that might outrank a newly discovered dynamic route.
Although the flattened branches contain ranking metadata, determining whether two route patterns overlap requires symbolic route-pattern analysis. Handling optional segments, splats, pathless layouts, partial parameters, case sensitivity, and equal-ranked route ordering adds substantial complexity.
Doing this work at request time would make
/__manifestmore expensive. Precomputing it would still add build complexity and could produce large competition sets.Shipping the full route topology
React Router could send a lightweight route-matching tree up front while continuing to lazy-load module and asset metadata.
This removes the ambiguity entirely, but adds initial payload proportional to the total route count and weakens the primary benefit of lazy route discovery.
Prototype
A proof of concept implements:
unstable_getRouteDiscoveryCacheKeyon<HydratedRouter>;/product/:id;/__manifest.The prototype verifies that after discovering
/product/a, rendering or navigating to/product/bdoes not make another manifest request when both resolve to the"product"cache key.Open questions
unstable_getRouteDiscoveryCacheKeythe clearest name, or wouldunstable_getManifestCacheKeybetter communicate its purpose?{ pathname, pattern }, onlypattern, or the full partial matches?pathnamemanually?Request for feedback
The main questions are:
All reactions