Skip to content

Run loaders on navigation - #814

Open
pleek91 wants to merge 5 commits into
route-datafrom
run-loaders
Open

Run loaders on navigation#814
pleek91 wants to merge 5 commits into
route-datafrom
run-loaders

Conversation

@pleek91

@pleek91 pleek91 commented Aug 3, 2026

Copy link
Copy Markdown
Member

Description

Sixth step of #805, stacked on #812. This is the one that makes loaders do something.

const user = createRoute({ name: 'user', path: '/user/[id]' })
  .addView(UserView, { props: (route) => ({ id: route.params.id }) })
  .addLoader(async (route) => api.getUser(route.params.id))

// in a component
const route = useRoute()
const user = await route.data

To the store, a props getter and a loader are the same thing: a named callback belonging to one match. Both are set into the navigation's store together, and only what waits on them differs — props are awaited as what a view renders with, loaders are awaited only so a push or a rejection from one is still acted on. That is what makes loaders non-blocking: nothing on the render path reads them.

The kind is part of the key, so a view and a loader sharing a name on one route stay two values.

Who has data

Data is attached to a route as it becomes current, not as it is resolved — a route is resolved before the navigation that computes its data owns a store, so reading it any earlier subscribes to the store that navigation is about to replace.

Surface Data
RouterRouteuseRoute(), router.route yes
a props getter's route yes
a loader's route no
ResolvedRouterouter.resolve, router.find, hook to/from no

A props getter has it because one loader feeding several views is the reason data exists:

.addLoader(async (route) => api.getUser(route.params.id), { name: 'user' })
.addView(UserHeader, { props: async (route) => ({ user: await route.data.user }) })
.addView(UserSidebar, { props: async (route) => ({ user: await route.data.user }) })

A loader does not. A route's data includes what its own loaders are computing, so a loader reading it could only wait on itself, or on a sibling waiting back. Its route is a plain ResolvedRoute, and reading data anyway throws LoaderDataAccessError rather than hanging.

Both reach other routes through parent.props and parent.data, which point only upward:

.addLoader(async (route, { parent }) => api.getPosts(await parent.data))

So the dependency graph is acyclic by construction: loaders read upward, props read upward and their own route, nothing reads downward.

Since the store is no longer only about props, createPropStore is now createRouteValueStore (and usePropStore follows). "Value" is the word the store already used internally — getValue, ValueLocation, and #806's own title — and it stays distinct from route.data, which means loader data specifically. Nothing renamed here is exported from the package.

Worth a look

  • createRouterRoute snapshots the resolved route in a spread, so anything that changes per navigation has to be passed as a ref. data now is. title has the same problem and is left alone here.
  • The warning for stalling while prefetching now covers loader data and route data, not just parent props, so its wording is no longer parent-specific — one existing assertion moved with it. Happy to carry the relationship into the message instead, so the parent case keeps its original advice.
  • reactive()'s unwrap types cannot see through a generic route's data (it is a promise), so createRouterRoute asserts its return shape instead of inferring it.
  • The route a callback receives is a proxy over the resolved route rather than a copy, so the data it replaces is only read when something asks for it.

Prefetching loaders is the next PR: the loaders prefetch setting does not exist yet, so a loader only runs on navigation.

pleek91 and others added 2 commits August 3, 2026 00:58
It compared a copy of a parameter against the parameter it was copied from,
so the condition was never true. Its job — keeping a superseded navigation
from acting on its own props — is done by the ABANDONED response instead.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Loaders are stored per match alongside views, so a route's data can be
derived from its matches the same way its views are.
pleek91 added 2 commits August 3, 2026 17:33
Loaders are declared the same way views are, and both methods now rebuild
through withRouteMethods so chaining one never drops the other. A loader
name an ancestor already uses throws, since a route's data combines the
loaders of every match.
RouteDataOf combines the loaders of every match, so a child sees an
ancestor's data as its own, and flattens to the loader's data directly when
the only loader in the tree is unnamed.

Carried by ResolvedRouteWithData rather than by ResolvedRoute: only the
route being navigated to has data, so a route the router merely resolved
would carry a promise that could never settle.
@pleek91
pleek91 force-pushed the run-loaders branch 7 times, most recently from cdce248 to d9970e6 Compare August 4, 2026 14:44
A view's props getter and a loader are the same thing to the store: a named
callback belonging to one match. Both are set into the navigation's store
together, and only what waits on them differs — props are awaited as what a
view renders with, loaders are awaited only to act on a push or a rejection.

The kind is part of the key, so a view and a loader sharing a name on one
route stay two values.

Data is attached to a route as it becomes current, which is the only route
whose data anything is computing. A props getter is given it too, since one
loader feeding several views is the reason data exists.

A loader is not: a route's data includes what the loader itself is computing,
so reading it could only wait on itself, or on a sibling waiting back. What
is left reads only upward, so no arrangement of callbacks can wait on itself.

The store is no longer only about props, so it is now the route value store.
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