Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions .changeset/fix-on-before-navigate-after-redirect.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
---
'@tanstack/router-core': patch
---

fix(router-core): fire `onBeforeNavigate` on every navigation, including those following a `beforeLoad` `redirect(...)`

`onBeforeNavigate` was gated on `stores.redirect` being unset, but that store was populated whenever `beforeLoad` threw `redirect(...)` and never cleared. As a result, every navigation after the first in-`beforeLoad` redirect silently skipped the `onBeforeNavigate` emission (`onBeforeLoad` next to it never had this gate). This broke Sentry's TanStack Router tracing integration and any userland pattern (e.g. form-state persistence) that hooks into `onBeforeNavigate`.

The gate is removed so `onBeforeNavigate` now matches `onBeforeLoad` and fires on every navigation.

Fixes #3920.
10 changes: 4 additions & 6 deletions packages/router-core/src/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2483,12 +2483,10 @@ export class RouterCore<
const prevLocation = this.stores.resolvedLocation.get()
const locationChangeInfo = getLocationChangeInfo(next, prevLocation)

if (!this.stores.redirect.get()) {
this.emit({
type: 'onBeforeNavigate',
...locationChangeInfo,
})
}
this.emit({
type: 'onBeforeNavigate',
...locationChangeInfo,
})

this.emit({
type: 'onBeforeLoad',
Expand Down
52 changes: 51 additions & 1 deletion packages/router-core/tests/callbacks.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { describe, expect, it, test, vi } from 'vitest'
import { createMemoryHistory } from '@tanstack/history'
import { BaseRootRoute, BaseRoute } from '../src'
import { BaseRootRoute, BaseRoute, redirect } from '../src'
import { createTestRouter } from './routerTestUtils'

describe('callbacks', () => {
Expand Down Expand Up @@ -285,5 +285,55 @@ describe('callbacks', () => {
expect(events[0]).toMatchObject({ to: '/foo', pathChanged: true })
expect(events[1]).toMatchObject({ to: '/bar', pathChanged: true })
})

// Regression test for https://github.com/TanStack/router/issues/3920
// After a `beforeLoad` throws `redirect(...)`, `stores.redirect` was left
// populated forever, so the `!stores.redirect.get()` gate around the
// `onBeforeNavigate` emission suppressed the event for every subsequent
// navigation. This broke Sentry's TanStack Router tracing integration and
// any userland pattern (e.g. form-state persistence) that hooks into
// `onBeforeNavigate`.
it('fires on subsequent navigations after a redirect in beforeLoad (#3920)', async () => {
const rootRoute = new BaseRootRoute({})
const fooRoute = new BaseRoute({
getParentRoute: () => rootRoute,
path: '/foo',
beforeLoad: () => {
throw redirect({ to: '/bar' })
},
})
const barRoute = new BaseRoute({
getParentRoute: () => rootRoute,
path: '/bar',
})
const bazRoute = new BaseRoute({
getParentRoute: () => rootRoute,
path: '/baz',
})
const router = createTestRouter({
routeTree: rootRoute.addChildren([fooRoute, barRoute, bazRoute]),
history: createMemoryHistory(),
})

const onBeforeNavigate = vi.fn()
router.subscribe('onBeforeNavigate', onBeforeNavigate)

// First navigation: /foo triggers a redirect to /bar in beforeLoad.
await router.navigate({ to: '/foo' })
expect(onBeforeNavigate).toHaveBeenCalled()
const callsAfterRedirect = onBeforeNavigate.mock.calls.length

// Second, unrelated navigation must still emit onBeforeNavigate.
await router.navigate({ to: '/baz' })
expect(onBeforeNavigate.mock.calls.length).toBeGreaterThan(
callsAfterRedirect,
)
expect(onBeforeNavigate).toHaveBeenLastCalledWith(
expect.objectContaining({
type: 'onBeforeNavigate',
pathChanged: true,
}),
)
})
})
})