Skip to content

fix: include loader identity and params in loader cache key#224

Merged
uhyo merged 1 commit into
masterfrom
claude/issue-207-dynamic-routes-35zxzy
Jul 12, 2026
Merged

fix: include loader identity and params in loader cache key#224
uhyo merged 1 commit into
masterfrom
claude/issue-207-dynamic-routes-35zxzy

Conversation

@uhyo

@uhyo uhyo commented Jul 12, 2026

Copy link
Copy Markdown
Owner

Fixes #207

Problem

Loader cache keys were ${entryKey}:${matchIndex}, carrying no route identity. Whenever the same navigation entry mapped to a different route stack, the wrong cached data was served:

  1. Dynamic routes prop (feature flags, permission-dependent routes): a different route matching at the same index got the previous route's cached loader result instead of running its own loader.
  2. Two <Router> instances on one page: both share the module-level cache and the same navigation entry, so their keys collided.

Fix

Cache keys are now ${entryKey}:${matchIndex}:${loaderId}:${paramsKey}, where loaderId is a lazily-issued WeakMap id on the loader function and paramsKey is the serialized matched params.

Why loader-function identity (not the route definition object or the path pattern)

  • Path pattern doesn't fix the primary case: feature-flag variants typically share the same path with different loaders, so pattern-keyed entries would still serve stale data.
  • Route object identity breaks an existing guarantee (pinned by ssrLoaderCache.test.tsx): route definitions recreated across renders must keep their cached results. Object-identity keying would make inline-created routes re-execute loaders on every parent re-render.
  • Loader function identity + params is the semantically precise key: a cached value is fully determined by the loader and its args (params + URL, and the URL is already part of the entry key). Swapping in a route variant with a different loader closure re-executes it; recreated route objects that reference the same loader function keep their cache.

Params are included in the key so the same loader function reached via a different path pattern (e.g. /:page vs /:section) does not serve a result computed with different params for the same URL.

The prefix-based cache flows are unaffected since keys still start with the entry key: same-URL replace carry-over, form-submission revalidation, reload generations, and dispose cleanup all work unchanged.

Tests

New dynamicRoutes.test.tsx with 7 tests:

  • routes prop change on the same entry re-executes the new route's loader
  • flipping a flag back serves the previous variant's still-cached result
  • stable routes array across re-renders executes loaders once
  • recreated route objects with the same loader function don't re-execute
  • a shared loader re-executes when a routes change gives it different params
  • two <Router> instances on one page don't share loader data
  • route objects shared between route trees keep their cached results

Docs

Updated the "How Loaders Run" page: the caching section now describes the full cache identity, with a new "Changing the Routes Prop" subsection documenting the dynamic-routes behavior.

🤖 Generated with Claude Code

https://claude.ai/code/session_018waJv8AuctJWHBBWWkqTd5


Generated by Claude Code

Loader cache keys were `${entryKey}:${matchIndex}`, carrying no route
identity. Whenever the same navigation entry mapped to a different route
stack — a dynamic `routes` prop (feature flags, permission-dependent
routes) or two <Router> instances on one page — a different route
matching at the same index was served the previous route's cached loader
result instead of running its own loader.

Cache keys now also include the identity of the loader function (issued
lazily via a WeakMap) and the matched params. The loader function is
used as the identity — rather than the route definition object — because
a cached result is fully determined by the loader and its args: swapping
in a route variant with a different loader re-executes it, while route
definitions recreated across renders keep their cached results as long
as they reference the same loader function. Params are included so a
shared loader reached via a different path pattern (e.g. `/:page` vs
`/:section`) does not serve a result computed with different params.

Fixes #207

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_018waJv8AuctJWHBBWWkqTd5
@uhyo
uhyo merged commit 57815c4 into master Jul 12, 2026
1 check passed
@uhyo
uhyo deleted the claude/issue-207-dynamic-routes-35zxzy branch July 12, 2026 02:47
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.

Loader cache key contains no route identity — stale data with dynamic routes or multiple Routers

2 participants