-
-
Notifications
You must be signed in to change notification settings - Fork 1.6k
Description
Which project does this relate to?
Router
Describe the bug
The onLeave, onEnter, and onStay route lifecycle hooks use match.id to determine whether a route was entered, stayed, or left. However, match.id includes the loaderDepsHash, which means any change to loaderDeps (e.g. from search param updates) causes the match ID to change. This results in:
onLeavefiring on the old match (old deps hash)onEnterfiring on the new match (new deps hash)onStaynever firing
This is inconsistent with the loader's cause parameter, which correctly uses routeId (not match.id) to determine "stay" vs "enter":
// router.js — loader cause uses routeId ✅
const previousMatch = previousMatchesByRouteId.get(route.id);
const cause = previousMatch ? "stay" : "enter";// router.js — lifecycle hooks use match.id ❌
exitingMatches = previousMatches.filter(
(match) => !newMatches.some((d) => d.id === match.id)
);
enteringMatches = newMatches.filter(
(match) => !previousMatches.some((d) => d.id === match.id)
);
stayingMatches = newMatches.filter(
(match) => previousMatches.some((d) => d.id === match.id)
);The match ID is computed as route.id + interpolatedPath + loaderDepsHash, so when loaderDeps changes, the lifecycle hooks treat the same route as "left and re-entered" rather than "stayed".
Your Example Website or App
https://stackblitz.com/edit/github-1bd7mwws?file=src%2Froutes%2Findex.tsx
Steps to Reproduce the Bug or Issue
- Create a route with
loaderDepsthat depends on search params:
const DashboardRoute = createRoute({
path: '/dashboard',
validateSearch: DashboardSearchSchema,
loaderDeps: ({ search }) => search,
loader: async ({ context, deps }) => {
await context.queryClient.ensureQueryData(getDataQueryOptions(deps));
},
onLeave: () => console.log('onLeave'),
onStay: () => console.log('onStay'),
onEnter: () => console.log('onEnter'),
component: DashboardComponent,
});- Navigate to
/dashboard - Observe:
onEnterlogs (correct) - Change a search param via
navigate({ search: (s) => ({ ...s, page: 2 }) }) - Observe:
onLeaveandonEnterlog instead ofonStay
Expected behavior
When navigating within the same route and only search params change:
onStayshould fire (the route is still matched, just with different deps)onLeaveshould not fire (the user has not left the route)onEntershould not fire (the user was already on this route)
This would be consistent with the loader's cause parameter which correctly reports "stay" in this scenario.
Screenshots or Videos
No response
Platform
- Router Version: 1.163.2
- OS: macOS
- Browser: Chrome
- Bundler: Vite
Additional context
The practical impact is that onLeave cannot be used for cleanup logic that should only run when truly navigating away (e.g. invalidating TanStack Query caches). The workaround is to use a useEffect cleanup in the route component instead, but this defeats the purpose of having route-level lifecycle hooks.
The fix would likely involve using routeId instead of match.id when categorizing matches into exitingMatches, enteringMatches, and stayingMatches — the same approach already used for the loader's cause parameter.