@tanstack/react-router 1.97.26 → 1.98.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (55) hide show
  1. package/dist/cjs/Match.cjs +78 -28
  2. package/dist/cjs/Match.cjs.map +1 -1
  3. package/dist/cjs/Matches.cjs +4 -1
  4. package/dist/cjs/Matches.cjs.map +1 -1
  5. package/dist/cjs/Matches.d.cts +2 -0
  6. package/dist/cjs/ScriptOnce.cjs +1 -1
  7. package/dist/cjs/ScriptOnce.cjs.map +1 -1
  8. package/dist/cjs/ScrollRestoration.cjs +39 -0
  9. package/dist/cjs/ScrollRestoration.cjs.map +1 -0
  10. package/dist/cjs/ScrollRestoration.d.cts +15 -0
  11. package/dist/cjs/Transitioner.cjs +3 -33
  12. package/dist/cjs/Transitioner.cjs.map +1 -1
  13. package/dist/cjs/index.cjs +3 -4
  14. package/dist/cjs/index.cjs.map +1 -1
  15. package/dist/cjs/index.d.cts +2 -3
  16. package/dist/cjs/route.cjs.map +1 -1
  17. package/dist/cjs/route.d.cts +10 -2
  18. package/dist/cjs/router.cjs +37 -23
  19. package/dist/cjs/router.cjs.map +1 -1
  20. package/dist/cjs/router.d.cts +28 -27
  21. package/dist/cjs/scroll-restoration.cjs +168 -165
  22. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  23. package/dist/cjs/scroll-restoration.d.cts +25 -15
  24. package/dist/esm/Match.js +80 -30
  25. package/dist/esm/Match.js.map +1 -1
  26. package/dist/esm/Matches.d.ts +2 -0
  27. package/dist/esm/Matches.js +4 -1
  28. package/dist/esm/Matches.js.map +1 -1
  29. package/dist/esm/ScriptOnce.js +1 -1
  30. package/dist/esm/ScriptOnce.js.map +1 -1
  31. package/dist/esm/ScrollRestoration.d.ts +15 -0
  32. package/dist/esm/ScrollRestoration.js +39 -0
  33. package/dist/esm/ScrollRestoration.js.map +1 -0
  34. package/dist/esm/Transitioner.js +4 -34
  35. package/dist/esm/Transitioner.js.map +1 -1
  36. package/dist/esm/index.d.ts +2 -3
  37. package/dist/esm/index.js +1 -2
  38. package/dist/esm/route.d.ts +10 -2
  39. package/dist/esm/route.js.map +1 -1
  40. package/dist/esm/router.d.ts +28 -27
  41. package/dist/esm/router.js +38 -24
  42. package/dist/esm/router.js.map +1 -1
  43. package/dist/esm/scroll-restoration.d.ts +25 -15
  44. package/dist/esm/scroll-restoration.js +168 -148
  45. package/dist/esm/scroll-restoration.js.map +1 -1
  46. package/package.json +3 -3
  47. package/src/Match.tsx +101 -54
  48. package/src/Matches.tsx +3 -1
  49. package/src/ScriptOnce.tsx +1 -1
  50. package/src/ScrollRestoration.tsx +65 -0
  51. package/src/Transitioner.tsx +4 -40
  52. package/src/index.tsx +3 -3
  53. package/src/route.ts +38 -1
  54. package/src/router.ts +75 -49
  55. package/src/scroll-restoration.tsx +271 -183
package/src/Match.tsx CHANGED
@@ -5,6 +5,7 @@ import invariant from 'tiny-invariant'
5
5
  import warning from 'tiny-warning'
6
6
  import {
7
7
  createControlledPromise,
8
+ getLocationChangeInfo,
8
9
  pick,
9
10
  rootRouteId,
10
11
  } from '@tanstack/router-core'
@@ -16,6 +17,8 @@ import { isRedirect } from './redirects'
16
17
  import { matchContext } from './matchContext'
17
18
  import { SafeFragment } from './SafeFragment'
18
19
  import { renderRouteNotFound } from './renderRouteNotFound'
20
+ import { ScrollRestoration } from './scroll-restoration'
21
+ import type { ParsedLocation } from '@tanstack/router-core'
19
22
  import type { AnyRoute } from './route'
20
23
 
21
24
  export const Match = React.memo(function MatchImpl({
@@ -72,41 +75,88 @@ export const Match = React.memo(function MatchImpl({
72
75
  select: (s) => s.loadedAt,
73
76
  })
74
77
 
78
+ const parentRouteId = useRouterState({
79
+ select: (s) => {
80
+ const index = s.matches.findIndex((d) => d.id === matchId)
81
+ return s.matches[index - 1]?.routeId as string
82
+ },
83
+ })
84
+
75
85
  return (
76
- <matchContext.Provider value={matchId}>
77
- <ResolvedSuspenseBoundary fallback={pendingElement}>
78
- <ResolvedCatchBoundary
79
- getResetKey={() => resetKey}
80
- errorComponent={routeErrorComponent || ErrorComponent}
81
- onCatch={(error, errorInfo) => {
82
- // Forward not found errors (we don't want to show the error component for these)
83
- if (isNotFound(error)) throw error
84
- warning(false, `Error in route match: ${matchId}`)
85
- routeOnCatch?.(error, errorInfo)
86
- }}
87
- >
88
- <ResolvedNotFoundBoundary
89
- fallback={(error) => {
90
- // If the current not found handler doesn't exist or it has a
91
- // route ID which doesn't match the current route, rethrow the error
92
- if (
93
- !routeNotFoundComponent ||
94
- (error.routeId && error.routeId !== routeId) ||
95
- (!error.routeId && !route.isRoot)
96
- )
97
- throw error
98
-
99
- return React.createElement(routeNotFoundComponent, error as any)
86
+ <>
87
+ <matchContext.Provider value={matchId}>
88
+ <ResolvedSuspenseBoundary fallback={pendingElement}>
89
+ <ResolvedCatchBoundary
90
+ getResetKey={() => resetKey}
91
+ errorComponent={routeErrorComponent || ErrorComponent}
92
+ onCatch={(error, errorInfo) => {
93
+ // Forward not found errors (we don't want to show the error component for these)
94
+ if (isNotFound(error)) throw error
95
+ warning(false, `Error in route match: ${matchId}`)
96
+ routeOnCatch?.(error, errorInfo)
100
97
  }}
101
98
  >
102
- <MatchInner matchId={matchId} />
103
- </ResolvedNotFoundBoundary>
104
- </ResolvedCatchBoundary>
105
- </ResolvedSuspenseBoundary>
106
- </matchContext.Provider>
99
+ <ResolvedNotFoundBoundary
100
+ fallback={(error) => {
101
+ // If the current not found handler doesn't exist or it has a
102
+ // route ID which doesn't match the current route, rethrow the error
103
+ if (
104
+ !routeNotFoundComponent ||
105
+ (error.routeId && error.routeId !== routeId) ||
106
+ (!error.routeId && !route.isRoot)
107
+ )
108
+ throw error
109
+
110
+ return React.createElement(routeNotFoundComponent, error as any)
111
+ }}
112
+ >
113
+ <MatchInner matchId={matchId} />
114
+ </ResolvedNotFoundBoundary>
115
+ </ResolvedCatchBoundary>
116
+ </ResolvedSuspenseBoundary>
117
+ </matchContext.Provider>
118
+ {parentRouteId === rootRouteId ? (
119
+ <>
120
+ <OnRendered />
121
+ <ScrollRestoration />
122
+ </>
123
+ ) : null}
124
+ </>
107
125
  )
108
126
  })
109
127
 
128
+ // On Rendered can't happen above the root layout because it actually
129
+ // renders a dummy dom element to track the rendered state of the app.
130
+ // We render a script tag with a key that changes based on the current
131
+ // location state.key. Also, because it's below the root layout, it
132
+ // allows us to fire onRendered events even after a hydration mismatch
133
+ // error that occurred above the root layout (like bad head/link tags,
134
+ // which is common).
135
+ function OnRendered() {
136
+ const router = useRouter()
137
+
138
+ const prevLocationRef = React.useRef<undefined | ParsedLocation<{}>>(
139
+ undefined,
140
+ )
141
+
142
+ return (
143
+ <script
144
+ key={router.state.resolvedLocation?.state.key}
145
+ suppressHydrationWarning
146
+ ref={(el) => {
147
+ if (el) {
148
+ router.emit({
149
+ type: 'onRendered',
150
+ ...getLocationChangeInfo(router.state),
151
+ })
152
+ } else {
153
+ prevLocationRef.current = router.state.resolvedLocation
154
+ }
155
+ }}
156
+ />
157
+ )
158
+ }
159
+
110
160
  export const MatchInner = React.memo(function MatchInnerImpl({
111
161
  matchId,
112
162
  }: {
@@ -114,45 +164,41 @@ export const MatchInner = React.memo(function MatchInnerImpl({
114
164
  }): any {
115
165
  const router = useRouter()
116
166
 
117
- const { match, matchIndex, routeId } = useRouterState({
167
+ const { match, key, routeId } = useRouterState({
118
168
  select: (s) => {
119
169
  const matchIndex = s.matches.findIndex((d) => d.id === matchId)
120
170
  const match = s.matches[matchIndex]!
121
171
  const routeId = match.routeId as string
172
+
173
+ const remountFn =
174
+ (router.routesById[routeId] as AnyRoute).options.remountDeps ??
175
+ router.options.defaultRemountDeps
176
+ const remountDeps = remountFn?.({
177
+ routeId,
178
+ loaderDeps: match.loaderDeps,
179
+ params: match._strictParams,
180
+ search: match._strictSearch,
181
+ })
182
+ const key = remountDeps ? JSON.stringify(remountDeps) : undefined
183
+
122
184
  return {
185
+ key,
123
186
  routeId,
124
- matchIndex,
125
187
  match: pick(match, ['id', 'status', 'error']),
126
188
  }
127
189
  },
128
190
  structuralSharing: true as any,
129
191
  })
130
192
 
131
- const route = router.routesById[routeId]!
193
+ const route = router.routesById[routeId] as AnyRoute
132
194
 
133
195
  const out = React.useMemo(() => {
134
196
  const Comp = route.options.component ?? router.options.defaultComponent
135
- return Comp ? <Comp /> : <Outlet />
136
- }, [route.options.component, router.options.defaultComponent])
137
-
138
- // function useChangedDiff(value: any) {
139
- // const ref = React.useRef(value)
140
- // const changed = ref.current !== value
141
- // if (changed) {
142
- // console.log(
143
- // 'Changed:',
144
- // value,
145
- // Object.fromEntries(
146
- // Object.entries(value).filter(
147
- // ([key, val]) => val !== ref.current[key],
148
- // ),
149
- // ),
150
- // )
151
- // }
152
- // ref.current = value
153
- // }
154
-
155
- // useChangedDiff(match)
197
+ if (Comp) {
198
+ return <Comp key={key} />
199
+ }
200
+ return <Outlet />
201
+ }, [key, route.options.component, router.options.defaultComponent])
156
202
 
157
203
  const RouteErrorComponent =
158
204
  (route.options.errorComponent ?? router.options.defaultErrorComponent) ||
@@ -184,7 +230,8 @@ export const MatchInner = React.memo(function MatchInnerImpl({
184
230
  if (router.isServer) {
185
231
  return (
186
232
  <RouteErrorComponent
187
- error={match.error}
233
+ error={match.error as any}
234
+ reset={undefined as any}
188
235
  info={{
189
236
  componentStack: '',
190
237
  }}
package/src/Matches.tsx CHANGED
@@ -63,6 +63,7 @@ export interface RouteMatch<
63
63
  index: number
64
64
  pathname: string
65
65
  params: TAllParams
66
+ _strictParams: TAllParams
66
67
  status: 'pending' | 'success' | 'error' | 'redirected' | 'notFound'
67
68
  isFetching: false | 'beforeLoad' | 'loader'
68
69
  error: unknown
@@ -77,6 +78,7 @@ export interface RouteMatch<
77
78
  __beforeLoadContext: Record<string, unknown>
78
79
  context: TAllContext
79
80
  search: TFullSearchSchema
81
+ _strictSearch: TFullSearchSchema
80
82
  fetchCount: number
81
83
  abortController: AbortController
82
84
  cause: 'preload' | 'enter' | 'stay'
@@ -197,7 +199,7 @@ export function useMatchRoute<TRouter extends AnyRouter = RegisteredRouter>() {
197
199
  const router = useRouter()
198
200
 
199
201
  useRouterState({
200
- select: (s) => [s.location.href, s.resolvedLocation.href, s.status],
202
+ select: (s) => [s.location.href, s.resolvedLocation?.href, s.status],
201
203
  structuralSharing: true as any,
202
204
  })
203
205
 
@@ -22,7 +22,7 @@ export function ScriptOnce({
22
22
  ? `console.info(\`Injected From Server:
23
23
  ${jsesc(children.toString(), { quotes: 'backtick' })}\`)`
24
24
  : '',
25
- 'if (typeof __TSR__ !== "undefined") __TSR__.cleanScripts()',
25
+ 'if (typeof __TSR_SSR__ !== "undefined") __TSR_SSR__.cleanScripts()',
26
26
  ]
27
27
  .filter(Boolean)
28
28
  .join('\n'),
@@ -0,0 +1,65 @@
1
+ import { useRouter } from './useRouter'
2
+ import {
3
+ defaultGetScrollRestorationKey,
4
+ getCssSelector,
5
+ scrollRestorationCache,
6
+ setupScrollRestoration,
7
+ } from './scroll-restoration'
8
+ import type { ScrollRestorationOptions } from './scroll-restoration'
9
+ import type { ParsedLocation } from '@tanstack/router-core'
10
+
11
+ function useScrollRestoration() {
12
+ const router = useRouter()
13
+ setupScrollRestoration(router, true)
14
+ }
15
+
16
+ /**
17
+ * @deprecated use createRouter's `scrollRestoration` option instead
18
+ */
19
+ export function ScrollRestoration(_props: ScrollRestorationOptions) {
20
+ useScrollRestoration()
21
+
22
+ if (process.env.NODE_ENV === 'development') {
23
+ console.warn(
24
+ "The ScrollRestoration component is deprecated. Use createRouter's `scrollRestoration` option instead.",
25
+ )
26
+ }
27
+
28
+ return null
29
+ }
30
+
31
+ export function useElementScrollRestoration(
32
+ options: (
33
+ | {
34
+ id: string
35
+ getElement?: () => Element | undefined | null
36
+ }
37
+ | {
38
+ id?: string
39
+ getElement: () => Element | undefined | null
40
+ }
41
+ ) & {
42
+ getKey?: (location: ParsedLocation) => string
43
+ },
44
+ ) {
45
+ useScrollRestoration()
46
+
47
+ const router = useRouter()
48
+ const getKey = options.getKey || defaultGetScrollRestorationKey
49
+
50
+ let elementSelector = ''
51
+
52
+ if (options.id) {
53
+ elementSelector = `[data-scroll-restoration-id="${options.id}"]`
54
+ } else {
55
+ const element = options.getElement?.()
56
+ if (!element) {
57
+ return
58
+ }
59
+ elementSelector = getCssSelector(element)
60
+ }
61
+
62
+ const restoreKey = getKey(router.latestLocation)
63
+ const byKey = scrollRestorationCache.state[restoreKey]
64
+ return byKey?.[elementSelector]
65
+ }
@@ -1,5 +1,5 @@
1
1
  import * as React from 'react'
2
- import { trimPathRight } from '@tanstack/router-core'
2
+ import { getLocationChangeInfo, trimPathRight } from '@tanstack/router-core'
3
3
  import { useLayoutEffect, usePrevious } from './utils'
4
4
  import { useRouter } from './useRouter'
5
5
  import { useRouterState } from './useRouterState'
@@ -87,17 +87,9 @@ export function Transitioner() {
87
87
  useLayoutEffect(() => {
88
88
  // The router was loading and now it's not
89
89
  if (previousIsLoading && !isLoading) {
90
- const toLocation = router.state.location
91
- const fromLocation = router.state.resolvedLocation
92
- const pathChanged = fromLocation.pathname !== toLocation.pathname
93
- const hrefChanged = fromLocation.href !== toLocation.href
94
-
95
90
  router.emit({
96
91
  type: 'onLoad', // When the new URL has committed, when the new matches have been loaded into state.matches
97
- fromLocation,
98
- toLocation,
99
- pathChanged,
100
- hrefChanged,
92
+ ...getLocationChangeInfo(router.state),
101
93
  })
102
94
  }
103
95
  }, [previousIsLoading, router, isLoading])
@@ -105,17 +97,9 @@ export function Transitioner() {
105
97
  useLayoutEffect(() => {
106
98
  // emit onBeforeRouteMount
107
99
  if (previousIsPagePending && !isPagePending) {
108
- const toLocation = router.state.location
109
- const fromLocation = router.state.resolvedLocation
110
- const pathChanged = fromLocation.pathname !== toLocation.pathname
111
- const hrefChanged = fromLocation.href !== toLocation.href
112
-
113
100
  router.emit({
114
101
  type: 'onBeforeRouteMount',
115
- fromLocation,
116
- toLocation,
117
- pathChanged,
118
- hrefChanged,
102
+ ...getLocationChangeInfo(router.state),
119
103
  })
120
104
  }
121
105
  }, [isPagePending, previousIsPagePending, router])
@@ -123,17 +107,9 @@ export function Transitioner() {
123
107
  useLayoutEffect(() => {
124
108
  // The router was pending and now it's not
125
109
  if (previousIsAnyPending && !isAnyPending) {
126
- const toLocation = router.state.location
127
- const fromLocation = router.state.resolvedLocation
128
- const pathChanged = fromLocation.pathname !== toLocation.pathname
129
- const hrefChanged = fromLocation.href !== toLocation.href
130
-
131
110
  router.emit({
132
111
  type: 'onResolved',
133
- fromLocation,
134
- toLocation,
135
- pathChanged,
136
- hrefChanged,
112
+ ...getLocationChangeInfo(router.state),
137
113
  })
138
114
 
139
115
  router.__store.setState((s) => ({
@@ -141,18 +117,6 @@ export function Transitioner() {
141
117
  status: 'idle',
142
118
  resolvedLocation: s.location,
143
119
  }))
144
-
145
- if (typeof document !== 'undefined' && (document as any).querySelector) {
146
- const hashScrollIntoViewOptions =
147
- router.state.location.state.__hashScrollIntoViewOptions ?? true
148
-
149
- if (hashScrollIntoViewOptions && router.state.location.hash !== '') {
150
- const el = document.getElementById(router.state.location.hash)
151
- if (el) {
152
- el.scrollIntoView(hashScrollIntoViewOptions)
153
- }
154
- }
155
- }
156
120
  }
157
121
  }, [isAnyPending, previousIsAnyPending, router])
158
122
 
package/src/index.tsx CHANGED
@@ -259,6 +259,8 @@ export type {
259
259
  BeforeLoadContextParameter,
260
260
  ResolveAllContext,
261
261
  ResolveAllParamsFromParent,
262
+ MakeRemountDepsOptionsUnion,
263
+ RemountDepsOptions,
262
264
  } from './route'
263
265
 
264
266
  export type {
@@ -315,11 +317,9 @@ export type {
315
317
  } from './RouterProvider'
316
318
 
317
319
  export {
318
- useScrollRestoration,
319
320
  useElementScrollRestoration,
320
321
  ScrollRestoration,
321
- } from './scroll-restoration'
322
- export type { ScrollRestorationOptions } from './scroll-restoration'
322
+ } from './ScrollRestoration'
323
323
 
324
324
  export type { UseBlockerOpts, ShouldBlockFn } from './useBlocker'
325
325
  export { useBlocker, Block } from './useBlocker'
package/src/route.ts CHANGED
@@ -59,7 +59,7 @@ import type {
59
59
  RouteMatch,
60
60
  } from './Matches'
61
61
  import type { NavigateOptions, ToMaskOptions } from './link'
62
- import type { RouteById, RouteIds, RoutePaths } from './routeInfo'
62
+ import type { ParseRoute, RouteById, RouteIds, RoutePaths } from './routeInfo'
63
63
  import type { AnyRouter, RegisteredRouter, Router } from './router'
64
64
  import type { BuildLocationFn, NavigateFn } from './RouterProvider'
65
65
  import type { NotFoundError } from './not-found'
@@ -154,6 +154,7 @@ export type FileBaseRouteOptions<
154
154
  TRouterContext = {},
155
155
  TRouteContextFn = AnyContext,
156
156
  TBeforeLoadFn = AnyContext,
157
+ TRemountDepsFn = AnyContext,
157
158
  > = ParamsOptions<TPath, TParams> & {
158
159
  validateSearch?: Constrain<TSearchValidator, AnyValidator, DefaultValidator>
159
160
 
@@ -204,6 +205,18 @@ export type FileBaseRouteOptions<
204
205
  opts: FullSearchSchemaOption<TParentRoute, TSearchValidator>,
205
206
  ) => TLoaderDeps
206
207
 
208
+ remountDeps?: Constrain<
209
+ TRemountDepsFn,
210
+ (
211
+ opt: RemountDepsOptions<
212
+ TId,
213
+ FullSearchSchemaOption<TParentRoute, TSearchValidator>,
214
+ Expand<ResolveAllParamsFromParent<TParentRoute, TParams>>,
215
+ TLoaderDeps
216
+ >,
217
+ ) => any
218
+ >
219
+
207
220
  loader?: Constrain<
208
221
  TLoaderFn,
209
222
  (
@@ -275,6 +288,30 @@ export interface RouteContextOptions<
275
288
  context: Expand<RouteContextParameter<TParentRoute, TRouterContext>>
276
289
  }
277
290
 
291
+ export interface RemountDepsOptions<
292
+ in out TRouteId,
293
+ in out TFullSearchSchema,
294
+ in out TAllParams,
295
+ in out TLoaderDeps,
296
+ > {
297
+ routeId: TRouteId
298
+ search: TFullSearchSchema
299
+ params: TAllParams
300
+ loaderDeps: TLoaderDeps
301
+ }
302
+
303
+ export type MakeRemountDepsOptionsUnion<
304
+ TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
305
+ TRoute extends AnyRoute = ParseRoute<TRouteTree>,
306
+ > = TRoute extends any
307
+ ? RemountDepsOptions<
308
+ TRoute['id'],
309
+ TRoute['types']['fullSearchSchema'],
310
+ TRoute['types']['allParams'],
311
+ TRoute['types']['loaderDeps']
312
+ >
313
+ : never
314
+
278
315
  export interface BeforeLoadContextOptions<
279
316
  in out TParentRoute extends AnyRoute,
280
317
  in out TSearchValidator,