diff --git a/packages/react-router/tests/issue-8128-descendant-lifecycle-context.test.tsx b/packages/react-router/tests/issue-8128-descendant-lifecycle-context.test.tsx
new file mode 100644
index 0000000000..3a648c3326
--- /dev/null
+++ b/packages/react-router/tests/issue-8128-descendant-lifecycle-context.test.tsx
@@ -0,0 +1,64 @@
+import { cleanup, render, screen } from '@testing-library/react'
+import { afterEach, expect, test, vi } from 'vitest'
+import {
+ RouterProvider,
+ createMemoryHistory,
+ createRootRoute,
+ createRoute,
+ createRouter,
+ notFound,
+} from '../src'
+
+afterEach(() => {
+ cleanup()
+ vi.restoreAllMocks()
+})
+
+test('a structural descendant below a not-found boundary does not run onEnter when its component cannot render', async () => {
+ const parentOnEnter = vi.fn()
+ const unavailableOnEnter = vi.fn()
+ const descendantOnEnter = vi.fn()
+
+ const rootRoute = createRootRoute()
+ const tenantRoute = createRoute({
+ onEnter: parentOnEnter,
+ getParentRoute: () => rootRoute,
+ path: 'tenants/$tenantId',
+ })
+ const unavailableSettingsRoute = createRoute({
+ onEnter: unavailableOnEnter,
+ getParentRoute: () => tenantRoute,
+ path: 'settings',
+ beforeLoad: () => {
+ throw notFound()
+ },
+ notFoundComponent: () =>
Tenant settings unavailable
,
+ })
+ const profileRoute = createRoute({
+ getParentRoute: () => unavailableSettingsRoute,
+ path: 'profile',
+ onEnter: descendantOnEnter,
+ component: () => Profile settings
,
+ })
+ const router = createRouter({
+ routeTree: rootRoute.addChildren([
+ tenantRoute.addChildren([
+ unavailableSettingsRoute.addChildren([profileRoute]),
+ ]),
+ ]),
+ history: createMemoryHistory({
+ initialEntries: ['/tenants/acme/settings/profile'],
+ }),
+ })
+
+ render()
+
+ expect(
+ await screen.findByText('Tenant settings unavailable'),
+ ).toBeInTheDocument()
+
+ expect(screen.queryByText('Profile settings')).not.toBeInTheDocument()
+ expect(parentOnEnter).toHaveBeenCalled()
+ expect(unavailableOnEnter).toHaveBeenCalled()
+ expect(descendantOnEnter).not.toHaveBeenCalled()
+})
diff --git a/packages/router-core/src/router.ts b/packages/router-core/src/router.ts
index 900609098e..b156f8872a 100644
--- a/packages/router-core/src/router.ts
+++ b/packages/router-core/src/router.ts
@@ -938,24 +938,49 @@ export function runRouteLifecycle(
if (owner && router._tx !== owner) {
return
}
- if (!matches.some((candidate) => candidate.routeId === match.routeId)) {
+ if (
+ matches.find(
+ (candidate) =>
+ candidate.routeId === match.routeId ||
+ candidate.status === 'error' ||
+ candidate.status === 'notFound' ||
+ candidate._notFound,
+ )?.routeId !== match.routeId
+ ) {
;(router.routesById as Record)[
match.routeId
]!.options.onLeave?.(match)
}
+ if (
+ match.status === 'error' ||
+ match.status === 'notFound' ||
+ match._notFound
+ ) {
+ break
+ }
}
for (const match of matches) {
if (owner && router._tx !== owner) {
return
}
- const route = (router.routesById as Record)[
- match.routeId
- ]!
- route.options[
- previous.some((candidate) => candidate.routeId === match.routeId)
+ ;(router.routesById as Record)[match.routeId]!.options[
+ previous.find(
+ (candidate) =>
+ candidate.routeId === match.routeId ||
+ candidate.status === 'error' ||
+ candidate.status === 'notFound' ||
+ candidate._notFound,
+ )?.routeId === match.routeId
? 'onStay'
: 'onEnter'
]?.(match)
+ if (
+ match.status === 'error' ||
+ match.status === 'notFound' ||
+ match._notFound
+ ) {
+ break
+ }
}
}