Skip to content

Commit 6cbd96e

Browse files
refactor: don't reparse upon navigation (#6398)
1 parent 46ff9fa commit 6cbd96e

18 files changed

Lines changed: 530 additions & 146 deletions

File tree

e2e/react-router/basic-file-based-code-splitting/src/routeTree.gen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -171,7 +171,7 @@ declare module '@tanstack/react-router' {
171171
'/_layout': {
172172
id: '/_layout'
173173
path: ''
174-
fullPath: ''
174+
fullPath: '/'
175175
preLoaderRoute: typeof LayoutRouteImport
176176
parentRoute: typeof rootRouteImport
177177
}
@@ -199,7 +199,7 @@ declare module '@tanstack/react-router' {
199199
'/_layout/_layout-2': {
200200
id: '/_layout/_layout-2'
201201
path: ''
202-
fullPath: ''
202+
fullPath: '/'
203203
preLoaderRoute: typeof LayoutLayout2RouteImport
204204
parentRoute: typeof LayoutRoute
205205
}

e2e/react-router/basic-react-query-file-based/src/routeTree.gen.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -124,7 +124,7 @@ declare module '@tanstack/react-router' {
124124
'/_layout': {
125125
id: '/_layout'
126126
path: ''
127-
fullPath: ''
127+
fullPath: '/'
128128
preLoaderRoute: typeof LayoutRouteImport
129129
parentRoute: typeof rootRouteImport
130130
}
@@ -152,7 +152,7 @@ declare module '@tanstack/react-router' {
152152
'/_layout/_layout-2': {
153153
id: '/_layout/_layout-2'
154154
path: ''
155-
fullPath: ''
155+
fullPath: '/'
156156
preLoaderRoute: typeof LayoutLayout2RouteImport
157157
parentRoute: typeof LayoutRoute
158158
}

e2e/vue-start/custom-basepath/src/routeTree.gen.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,7 @@ export interface FileRoutesByFullPath {
106106
'/redirect/throw-it': typeof RedirectThrowItRoute
107107
'/users/$userId': typeof UsersUserIdRoute
108108
'/posts/': typeof PostsIndexRoute
109-
'/redirect': typeof RedirectIndexRoute
109+
'/redirect/': typeof RedirectIndexRoute
110110
'/users/': typeof UsersIndexRoute
111111
'/api/users/$userId': typeof ApiUsersUserIdRoute
112112
'/posts/$postId/deep': typeof PostsPostIdDeepRoute
@@ -155,7 +155,7 @@ export interface FileRouteTypes {
155155
| '/redirect/throw-it'
156156
| '/users/$userId'
157157
| '/posts/'
158-
| '/redirect'
158+
| '/redirect/'
159159
| '/users/'
160160
| '/api/users/$userId'
161161
| '/posts/$postId/deep'
@@ -250,7 +250,7 @@ declare module '@tanstack/vue-router' {
250250
'/redirect/': {
251251
id: '/redirect/'
252252
path: '/redirect'
253-
fullPath: '/redirect'
253+
fullPath: '/redirect/'
254254
preLoaderRoute: typeof RedirectIndexRouteImport
255255
parentRoute: typeof rootRouteImport
256256
}

packages/history/src/index.ts

Lines changed: 47 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,13 @@
44

55
export interface NavigateOptions {
66
ignoreBlocker?: boolean
7+
/** When true, Transitioner should skip calling load() - commitLocation handles it */
8+
skipTransitionerLoad?: boolean
79
}
810

11+
/** Result of a navigation attempt (push/replace) */
12+
export type NavigationResult = { type: 'SUCCESS' } | { type: 'BLOCKED' }
13+
914
type SubscriberHistoryAction =
1015
| {
1116
type: Exclude<HistoryAction, 'GO'>
@@ -15,18 +20,27 @@ type SubscriberHistoryAction =
1520
index: number
1621
}
1722

18-
type SubscriberArgs = {
23+
export type SubscriberArgs = {
1924
location: HistoryLocation
2025
action: SubscriberHistoryAction
26+
navigateOpts?: NavigateOptions
2127
}
2228

2329
export interface RouterHistory {
2430
location: HistoryLocation
2531
length: number
2632
subscribers: Set<(opts: SubscriberArgs) => void>
2733
subscribe: (cb: (opts: SubscriberArgs) => void) => () => void
28-
push: (path: string, state?: any, navigateOpts?: NavigateOptions) => void
29-
replace: (path: string, state?: any, navigateOpts?: NavigateOptions) => void
34+
push: (
35+
path: string,
36+
state?: any,
37+
navigateOpts?: NavigateOptions,
38+
) => Promise<NavigationResult>
39+
replace: (
40+
path: string,
41+
state?: any,
42+
navigateOpts?: NavigateOptions,
43+
) => Promise<NavigationResult>
3044
go: (index: number, navigateOpts?: NavigateOptions) => void
3145
back: (navigateOpts?: NavigateOptions) => void
3246
forward: (navigateOpts?: NavigateOptions) => void
@@ -56,6 +70,21 @@ export type ParsedHistoryState = HistoryState & {
5670
key?: string // TODO: Remove in v2 - use __TSR_key instead
5771
__TSR_key?: string
5872
__TSR_index: number
73+
/** Whether to reset scroll position on this navigation (default: true) */
74+
__TSR_resetScroll?: boolean
75+
/** Session id for cached TSR internals */
76+
__TSR_sessionId?: string
77+
/** Match snapshot for fast-path on back/forward navigation */
78+
__TSR_matches?: {
79+
routeIds: Array<string>
80+
params: Record<string, string>
81+
globalNotFoundRouteId?: string
82+
searchStr?: string
83+
validatedSearches?: Array<{
84+
search: Record<string, unknown>
85+
strictSearch: Record<string, unknown>
86+
}>
87+
}
5988
}
6089

6190
type ShouldAllowNavigation = any
@@ -116,9 +145,14 @@ export function createHistory(opts: {
116145
let location = opts.getLocation()
117146
const subscribers = new Set<(opts: SubscriberArgs) => void>()
118147

119-
const notify = (action: SubscriberHistoryAction) => {
148+
const notify = (
149+
action: SubscriberHistoryAction,
150+
navigateOpts?: NavigateOptions,
151+
) => {
120152
location = opts.getLocation()
121-
subscribers.forEach((subscriber) => subscriber({ location, action }))
153+
subscribers.forEach((subscriber) =>
154+
subscriber({ location, action, navigateOpts }),
155+
)
122156
}
123157

124158
const handleIndexChange = (action: SubscriberHistoryAction) => {
@@ -130,11 +164,11 @@ export function createHistory(opts: {
130164
task,
131165
navigateOpts,
132166
...actionInfo
133-
}: TryNavigateArgs) => {
167+
}: TryNavigateArgs): Promise<NavigationResult> => {
134168
const ignoreBlocker = navigateOpts?.ignoreBlocker ?? false
135169
if (ignoreBlocker) {
136170
task()
137-
return
171+
return { type: 'SUCCESS' }
138172
}
139173

140174
const blockers = opts.getBlockers?.() ?? []
@@ -150,12 +184,13 @@ export function createHistory(opts: {
150184
})
151185
if (isBlocked) {
152186
opts.onBlocked?.()
153-
return
187+
return { type: 'BLOCKED' }
154188
}
155189
}
156190
}
157191

158192
task()
193+
return { type: 'SUCCESS' }
159194
}
160195

161196
return {
@@ -176,10 +211,10 @@ export function createHistory(opts: {
176211
push: (path, state, navigateOpts) => {
177212
const currentIndex = location.state[stateIndexKey]
178213
state = assignKeyAndIndex(currentIndex + 1, state)
179-
tryNavigation({
214+
return tryNavigation({
180215
task: () => {
181216
opts.pushState(path, state)
182-
notify({ type: 'PUSH' })
217+
notify({ type: 'PUSH' }, navigateOpts)
183218
},
184219
navigateOpts,
185220
type: 'PUSH',
@@ -190,10 +225,10 @@ export function createHistory(opts: {
190225
replace: (path, state, navigateOpts) => {
191226
const currentIndex = location.state[stateIndexKey]
192227
state = assignKeyAndIndex(currentIndex, state)
193-
tryNavigation({
228+
return tryNavigation({
194229
task: () => {
195230
opts.replaceState(path, state)
196-
notify({ type: 'REPLACE' })
231+
notify({ type: 'REPLACE' }, navigateOpts)
197232
},
198233
navigateOpts,
199234
type: 'REPLACE',

packages/react-router/src/Transitioner.tsx

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
import { useLayoutEffect, usePrevious } from './utils'
88
import { useRouter } from './useRouter'
99
import { useRouterState } from './useRouterState'
10+
import type { SubscriberArgs } from '@tanstack/history'
1011

1112
export function Transitioner() {
1213
const router = useRouter()
@@ -41,7 +42,17 @@ export function Transitioner() {
4142
// Subscribe to location changes
4243
// and try to load the new location
4344
React.useEffect(() => {
44-
const unsub = router.history.subscribe(router.load)
45+
const unsub = router.history.subscribe(
46+
({ navigateOpts }: SubscriberArgs) => {
47+
// If commitLocation initiated this navigation, it handles load() itself
48+
if (navigateOpts?.skipTransitionerLoad) {
49+
return
50+
}
51+
52+
// External navigation (pop, direct history.push, etc): call load normally
53+
router.load()
54+
},
55+
)
4556

4657
const nextLocation = router.buildLocation({
4758
to: router.latestLocation.pathname,

packages/react-router/tests/Matches.test.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ describe('matching on different param types', () => {
344344

345345
await act(() => render(<RouterProvider router={router} />))
346346

347-
act(() => router.history.push(nav))
347+
await act(() => router.history.push(nav))
348348

349349
const paramsToCheck = await screen.findByTestId('params')
350350
const matchesToCheck = await screen.findByTestId('matches')

packages/react-router/tests/useNavigate.test.tsx

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import {
77
fireEvent,
88
render,
99
screen,
10+
waitFor,
1011
} from '@testing-library/react'
1112

1213
import { z } from 'zod'
@@ -1316,9 +1317,11 @@ test('when setting search params with 2 parallel navigate calls', async () => {
13161317
})
13171318

13181319
render(<RouterProvider router={router} />)
1319-
expect(router.state.location.search).toEqual({
1320-
param1: 'param1-default',
1321-
param2: 'param2-default',
1320+
await waitFor(() => {
1321+
expect(router.state.location.search).toEqual({
1322+
param1: 'param1-default',
1323+
param2: 'param2-default',
1324+
})
13221325
})
13231326

13241327
const postsButton = await screen.findByRole('button', { name: 'search' })
@@ -1327,7 +1330,12 @@ test('when setting search params with 2 parallel navigate calls', async () => {
13271330

13281331
expect(await screen.findByTestId('param1')).toHaveTextContent('foo')
13291332
expect(await screen.findByTestId('param2')).toHaveTextContent('bar')
1330-
expect(router.state.location.search).toEqual({ param1: 'foo', param2: 'bar' })
1333+
await waitFor(() => {
1334+
expect(router.state.location.search).toEqual({
1335+
param1: 'foo',
1336+
param2: 'bar',
1337+
})
1338+
})
13311339
const search = new URLSearchParams(window.location.search)
13321340
expect(search.get('param1')).toEqual('foo')
13331341
expect(search.get('param2')).toEqual('bar')
@@ -1447,21 +1455,27 @@ test.each([true, false])(
14471455

14481456
fireEvent.click(postButton)
14491457

1450-
expect(router.state.location.pathname).toBe(`/post${tail}`)
1458+
await waitFor(() => {
1459+
expect(router.state.location.pathname).toBe(`/post${tail}`)
1460+
})
14511461

14521462
const searchButton = await screen.findByTestId('search-btn')
14531463

14541464
fireEvent.click(searchButton)
14551465

1456-
expect(router.state.location.pathname).toBe(`/post${tail}`)
1457-
expect(router.state.location.search).toEqual({ param1: 'value1' })
1466+
await waitFor(() => {
1467+
expect(router.state.location.pathname).toBe(`/post${tail}`)
1468+
expect(router.state.location.search).toEqual({ param1: 'value1' })
1469+
})
14581470

14591471
const searchButton2 = await screen.findByTestId('search2-btn')
14601472

14611473
fireEvent.click(searchButton2)
14621474

1463-
expect(router.state.location.pathname).toBe(`/post${tail}`)
1464-
expect(router.state.location.search).toEqual({ param1: 'value2' })
1475+
await waitFor(() => {
1476+
expect(router.state.location.pathname).toBe(`/post${tail}`)
1477+
expect(router.state.location.search).toEqual({ param1: 'value2' })
1478+
})
14651479
},
14661480
)
14671481

@@ -1760,22 +1774,28 @@ test.each([true, false])(
17601774

17611775
fireEvent.click(detail1AddBtn)
17621776

1763-
expect(router.state.location.pathname).toBe(`/posts/id1/detail${tail}`)
1764-
expect(router.state.location.search).toEqual({ _test: true })
1777+
await waitFor(() => {
1778+
expect(router.state.location.pathname).toBe(`/posts/id1/detail${tail}`)
1779+
expect(router.state.location.search).toEqual({ _test: true })
1780+
})
17651781

17661782
const detail1RemoveBtn = await screen.findByTestId('detail-btn-remove-1')
17671783

17681784
fireEvent.click(detail1RemoveBtn)
17691785

1770-
expect(router.state.location.pathname).toBe(`/posts/id1/detail${tail}`)
1771-
expect(router.state.location.search).toEqual({})
1786+
await waitFor(() => {
1787+
expect(router.state.location.pathname).toBe(`/posts/id1/detail${tail}`)
1788+
expect(router.state.location.search).toEqual({})
1789+
})
17721790

17731791
const detail2AddBtn = await screen.findByTestId('detail-btn-add-2')
17741792

17751793
fireEvent.click(detail2AddBtn)
17761794

1777-
expect(router.state.location.pathname).toBe(`/posts/id1/detail${tail}`)
1778-
expect(router.state.location.search).toEqual({ _test: true })
1795+
await waitFor(() => {
1796+
expect(router.state.location.pathname).toBe(`/posts/id1/detail${tail}`)
1797+
expect(router.state.location.search).toEqual({ _test: true })
1798+
})
17791799
},
17801800
)
17811801

packages/router-core/src/index.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,11 @@ export type {
6969
CreateLazyFileRoute,
7070
} from './fileRoute'
7171

72-
export type { ParsedLocation } from './location'
72+
export type {
73+
MatchSnapshot,
74+
ParsedLocation,
75+
ValidatedSearchEntry,
76+
} from './location'
7377
export type { Manifest, RouterManagedTag } from './manifest'
7478
export { isMatch } from './Matches'
7579
export type {

0 commit comments

Comments
 (0)