@tanstack/react-router 1.27.0 → 1.28.2

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 (54) hide show
  1. package/dist/cjs/Matches.cjs +45 -23
  2. package/dist/cjs/Matches.cjs.map +1 -1
  3. package/dist/cjs/Matches.d.cts +5 -6
  4. package/dist/cjs/fileRoute.cjs.map +1 -1
  5. package/dist/cjs/fileRoute.d.cts +13 -14
  6. package/dist/cjs/index.cjs +0 -1
  7. package/dist/cjs/index.cjs.map +1 -1
  8. package/dist/cjs/index.d.cts +2 -2
  9. package/dist/cjs/link.cjs.map +1 -1
  10. package/dist/cjs/link.d.cts +34 -24
  11. package/dist/cjs/redirects.cjs.map +1 -1
  12. package/dist/cjs/redirects.d.cts +3 -1
  13. package/dist/cjs/route.cjs.map +1 -1
  14. package/dist/cjs/route.d.cts +44 -47
  15. package/dist/cjs/routeInfo.d.cts +5 -7
  16. package/dist/cjs/router.cjs +374 -327
  17. package/dist/cjs/router.cjs.map +1 -1
  18. package/dist/cjs/router.d.cts +3 -3
  19. package/dist/cjs/useParams.cjs.map +1 -1
  20. package/dist/cjs/utils.cjs +20 -2
  21. package/dist/cjs/utils.cjs.map +1 -1
  22. package/dist/cjs/utils.d.cts +11 -4
  23. package/dist/esm/Matches.d.ts +5 -6
  24. package/dist/esm/Matches.js +46 -24
  25. package/dist/esm/Matches.js.map +1 -1
  26. package/dist/esm/fileRoute.d.ts +13 -14
  27. package/dist/esm/fileRoute.js.map +1 -1
  28. package/dist/esm/index.d.ts +2 -2
  29. package/dist/esm/index.js +1 -2
  30. package/dist/esm/link.d.ts +34 -24
  31. package/dist/esm/link.js.map +1 -1
  32. package/dist/esm/redirects.d.ts +3 -1
  33. package/dist/esm/redirects.js.map +1 -1
  34. package/dist/esm/route.d.ts +44 -47
  35. package/dist/esm/route.js.map +1 -1
  36. package/dist/esm/routeInfo.d.ts +5 -7
  37. package/dist/esm/router.d.ts +3 -3
  38. package/dist/esm/router.js +375 -328
  39. package/dist/esm/router.js.map +1 -1
  40. package/dist/esm/useParams.js.map +1 -1
  41. package/dist/esm/utils.d.ts +11 -4
  42. package/dist/esm/utils.js +20 -2
  43. package/dist/esm/utils.js.map +1 -1
  44. package/package.json +4 -2
  45. package/src/Matches.tsx +75 -37
  46. package/src/fileRoute.ts +17 -36
  47. package/src/index.tsx +0 -2
  48. package/src/link.tsx +189 -82
  49. package/src/redirects.ts +4 -2
  50. package/src/route.ts +149 -187
  51. package/src/routeInfo.ts +5 -7
  52. package/src/router.ts +501 -429
  53. package/src/useParams.tsx +2 -2
  54. package/src/utils.ts +41 -8
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-router",
3
- "version": "1.27.0",
3
+ "version": "1.28.2",
4
4
  "description": "",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -50,12 +50,14 @@
50
50
  ],
51
51
  "dependencies": {
52
52
  "@tanstack/react-store": "^0.2.1",
53
+ "@testing-library/react": "^15.0.2",
53
54
  "tiny-invariant": "^1.3.1",
54
55
  "tiny-warning": "^1.0.3",
55
56
  "@tanstack/history": "1.26.10"
56
57
  },
57
58
  "devDependencies": {
58
59
  "@vitejs/plugin-react": "^4.2.1",
60
+ "jsdom": "^24.0.0",
59
61
  "react": "^18.2.0",
60
62
  "react-dom": "^18.2.0",
61
63
  "zod": "^3.22.4"
@@ -68,7 +70,7 @@
68
70
  "clean": "rimraf ./dist && rimraf ./coverage",
69
71
  "test:eslint": "eslint --ext .ts,.tsx ./src",
70
72
  "test:types": "tsc --noEmit",
71
- "test:unit": "vitest --typecheck",
73
+ "test:unit": "vitest --typecheck --watch=false",
72
74
  "test:unit:dev": "pnpm run test:unit --watch",
73
75
  "test:build": "publint --strict",
74
76
  "build": "vite build"
package/src/Matches.tsx CHANGED
@@ -1,12 +1,18 @@
1
1
  import * as React from 'react'
2
2
  import invariant from 'tiny-invariant'
3
3
  import warning from 'tiny-warning'
4
+ import { set } from 'zod'
4
5
  import { CatchBoundary, ErrorComponent } from './CatchBoundary'
5
6
  import { useRouterState } from './useRouterState'
6
7
  import { useRouter } from './useRouter'
7
- import { isServer, pick } from './utils'
8
+ import { createControlledPromise, pick } from './utils'
8
9
  import { CatchNotFound, DefaultGlobalNotFound, isNotFound } from './not-found'
9
10
  import { isRedirect } from './redirects'
11
+ import {
12
+ type AnyRouter,
13
+ type RegisteredRouter,
14
+ type RouterState,
15
+ } from './router'
10
16
  import type { ResolveRelativePath, ToOptions } from './link'
11
17
  import type {
12
18
  AnyRoute,
@@ -23,8 +29,13 @@ import type {
23
29
  RouteIds,
24
30
  RoutePaths,
25
31
  } from './routeInfo'
26
- import type { AnyRouter, RegisteredRouter, RouterState } from './router'
27
- import type { DeepPartial, Expand, NoInfer, StrictOrFrom } from './utils'
32
+ import type {
33
+ ControlledPromise,
34
+ DeepPartial,
35
+ Expand,
36
+ NoInfer,
37
+ StrictOrFrom,
38
+ } from './utils'
28
39
 
29
40
  export const matchContext = React.createContext<string | undefined>(undefined)
30
41
 
@@ -41,12 +52,12 @@ export interface RouteMatch<
41
52
  : Expand<Partial<AllParams<TRouteTree>>>
42
53
  status: 'pending' | 'success' | 'error' | 'redirected' | 'notFound'
43
54
  isFetching: boolean
44
- showPending: boolean
45
55
  error: unknown
46
56
  paramsError: unknown
47
57
  searchError: unknown
48
58
  updatedAt: number
49
- loadPromise?: Promise<void>
59
+ loadPromise: ControlledPromise<void>
60
+ loaderPromise: Promise<RouteById<TRouteTree, TRouteId>['types']['loaderData']>
50
61
  loaderData?: RouteById<TRouteTree, TRouteId>['types']['loaderData']
51
62
  routeContext: RouteById<TRouteTree, TRouteId>['types']['routeContext']
52
63
  context: RouteById<TRouteTree, TRouteId>['types']['allContext']
@@ -64,22 +75,21 @@ export interface RouteMatch<
64
75
  loaderDeps: RouteById<TRouteTree, TRouteId>['types']['loaderDeps']
65
76
  preload: boolean
66
77
  invalid: boolean
67
- pendingPromise?: Promise<void>
68
78
  meta?: Array<JSX.IntrinsicElements['meta']>
69
79
  links?: Array<JSX.IntrinsicElements['link']>
70
80
  scripts?: Array<JSX.IntrinsicElements['script']>
71
81
  headers?: Record<string, string>
72
82
  globalNotFound?: boolean
73
83
  staticData: StaticDataRouteOption
84
+ minPendingPromise?: ControlledPromise<void>
74
85
  }
75
86
 
76
87
  export type AnyRouteMatch = RouteMatch<any, any>
77
88
 
78
89
  export function Matches() {
79
- const router = useRouter()
80
90
  const matchId = useRouterState({
81
91
  select: (s) => {
82
- return getRenderedMatches(s)[0]?.id
92
+ return s.matches[0]?.id
83
93
  },
84
94
  })
85
95
 
@@ -113,8 +123,7 @@ function SafeFragment(props: any) {
113
123
  export function Match({ matchId }: { matchId: string }) {
114
124
  const router = useRouter()
115
125
  const routeId = useRouterState({
116
- select: (s) =>
117
- getRenderedMatches(s).find((d) => d.id === matchId)?.routeId as string,
126
+ select: (s) => s.matches.find((d) => d.id === matchId)?.routeId as string,
118
127
  })
119
128
 
120
129
  invariant(
@@ -186,7 +195,7 @@ export function Match({ matchId }: { matchId: string }) {
186
195
  return React.createElement(routeNotFoundComponent, error as any)
187
196
  }}
188
197
  >
189
- <MatchInner matchId={matchId} pendingElement={pendingElement} />
198
+ <MatchInner matchId={matchId} />
190
199
  </ResolvedNotFoundBoundary>
191
200
  </ResolvedCatchBoundary>
192
201
  </ResolvedSuspenseBoundary>
@@ -196,26 +205,26 @@ export function Match({ matchId }: { matchId: string }) {
196
205
 
197
206
  function MatchInner({
198
207
  matchId,
199
- pendingElement,
208
+ // pendingElement,
200
209
  }: {
201
210
  matchId: string
202
- pendingElement: any
211
+ // pendingElement: any
203
212
  }): any {
204
213
  const router = useRouter()
205
214
  const routeId = useRouterState({
206
- select: (s) =>
207
- getRenderedMatches(s).find((d) => d.id === matchId)?.routeId as string,
215
+ select: (s) => s.matches.find((d) => d.id === matchId)?.routeId as string,
208
216
  })
209
217
 
210
218
  const route = router.routesById[routeId]!
211
219
 
212
220
  const match = useRouterState({
213
221
  select: (s) =>
214
- pick(getRenderedMatches(s).find((d) => d.id === matchId)!, [
222
+ pick(s.matches.find((d) => d.id === matchId)!, [
223
+ 'id',
215
224
  'status',
216
225
  'error',
217
- 'showPending',
218
226
  'loadPromise',
227
+ 'minPendingPromise',
219
228
  ]),
220
229
  })
221
230
 
@@ -258,7 +267,7 @@ function MatchInner({
258
267
  // of a suspense boundary. This is the only way to get
259
268
  // renderToPipeableStream to not hang indefinitely.
260
269
  // We'll serialize the error and rethrow it on the client.
261
- if (isServer) {
270
+ if (router.isServer) {
262
271
  return (
263
272
  <RouteErrorComponent
264
273
  error={match.error}
@@ -279,9 +288,47 @@ function MatchInner({
279
288
  }
280
289
 
281
290
  if (match.status === 'pending') {
282
- if (match.showPending) {
283
- return pendingElement
291
+ // We're pending, and if we have a minPendingMs, we need to wait for it
292
+ const pendingMinMs =
293
+ route.options.pendingMinMs ?? router.options.defaultPendingMinMs
294
+
295
+ if (pendingMinMs && !match.minPendingPromise) {
296
+ // Create a promise that will resolve after the minPendingMs
297
+
298
+ match.minPendingPromise = createControlledPromise()
299
+
300
+ if (!router.isServer) {
301
+ Promise.resolve().then(() => {
302
+ router.__store.setState((s) => ({
303
+ ...s,
304
+ matches: s.matches.map((d) =>
305
+ d.id === match.id
306
+ ? { ...d, minPendingPromise: createControlledPromise() }
307
+ : d,
308
+ ),
309
+ }))
310
+ })
311
+
312
+ setTimeout(() => {
313
+ // We've handled the minPendingPromise, so we can delete it
314
+ router.__store.setState((s) => {
315
+ return {
316
+ ...s,
317
+ matches: s.matches.map((d) =>
318
+ d.id === match.id
319
+ ? {
320
+ ...d,
321
+ minPendingPromise:
322
+ (d.minPendingPromise?.resolve(), undefined),
323
+ }
324
+ : d,
325
+ ),
326
+ }
327
+ })
328
+ }, pendingMinMs)
329
+ }
284
330
  }
331
+
285
332
  throw match.loadPromise
286
333
  }
287
334
 
@@ -306,15 +353,14 @@ export const Outlet = React.memo(function Outlet() {
306
353
  const router = useRouter()
307
354
  const matchId = React.useContext(matchContext)
308
355
  const routeId = useRouterState({
309
- select: (s) =>
310
- getRenderedMatches(s).find((d) => d.id === matchId)?.routeId as string,
356
+ select: (s) => s.matches.find((d) => d.id === matchId)?.routeId as string,
311
357
  })
312
358
 
313
359
  const route = router.routesById[routeId]!
314
360
 
315
361
  const { parentGlobalNotFound } = useRouterState({
316
362
  select: (s) => {
317
- const matches = getRenderedMatches(s)
363
+ const matches = s.matches
318
364
  const parentMatch = matches.find((d) => d.id === matchId)
319
365
  invariant(
320
366
  parentMatch,
@@ -328,7 +374,7 @@ export const Outlet = React.memo(function Outlet() {
328
374
 
329
375
  const childMatchId = useRouterState({
330
376
  select: (s) => {
331
- const matches = getRenderedMatches(s)
377
+ const matches = s.matches
332
378
  const index = matches.findIndex((d) => d.id === matchId)
333
379
  return matches[index + 1]?.id
334
380
  },
@@ -453,14 +499,6 @@ export function MatchRoute<
453
499
  return params ? props.children : null
454
500
  }
455
501
 
456
- export function getRenderedMatches<
457
- TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
458
- >(state: RouterState<TRouteTree>) {
459
- return state.pendingMatches?.some((d) => d.showPending)
460
- ? state.pendingMatches
461
- : state.matches
462
- }
463
-
464
502
  export function useMatch<
465
503
  TRouteTree extends AnyRoute = RegisteredRouter['routeTree'],
466
504
  TFrom extends RouteIds<TRouteTree> = RouteIds<TRouteTree>,
@@ -476,7 +514,7 @@ export function useMatch<
476
514
 
477
515
  const matchSelection = useRouterState({
478
516
  select: (state) => {
479
- const match = getRenderedMatches(state).find((d) =>
517
+ const match = state.matches.find((d) =>
480
518
  opts.from ? opts.from === d.routeId : d.id === nearestMatchId,
481
519
  )
482
520
 
@@ -491,7 +529,7 @@ export function useMatch<
491
529
  },
492
530
  })
493
531
 
494
- return matchSelection as any
532
+ return matchSelection as TSelected
495
533
  }
496
534
 
497
535
  export function useMatches<
@@ -506,7 +544,7 @@ export function useMatches<
506
544
  }): T {
507
545
  return useRouterState({
508
546
  select: (state) => {
509
- const matches = getRenderedMatches(state)
547
+ const matches = state.matches
510
548
  return opts?.select
511
549
  ? opts.select(matches as Array<TRouteMatch>)
512
550
  : (matches as T)
@@ -603,10 +641,10 @@ export function useLoaderData<
603
641
  ...opts,
604
642
  select: (s) => {
605
643
  return typeof opts.select === 'function'
606
- ? opts.select(s.loaderData)
644
+ ? opts.select(s.loaderData as TRouteMatch)
607
645
  : s.loaderData
608
646
  },
609
- })
647
+ }) as TSelected
610
648
  }
611
649
 
612
650
  export function isServerSideError(error: unknown): error is {
package/src/fileRoute.ts CHANGED
@@ -24,7 +24,7 @@ import type {
24
24
  TrimPathLeft,
25
25
  UpdatableRouteOptions,
26
26
  } from './route'
27
- import type { Assign, Expand, IsAny } from './utils'
27
+ import type { Assign, IsAny } from './utils'
28
28
  import type { RouteMatch } from './Matches'
29
29
  import type { NoInfer } from '@tanstack/react-store'
30
30
  import type { RegisteredRouter } from './router'
@@ -67,7 +67,7 @@ export type RemoveUnderScores<T extends string> = Replace<
67
67
  >
68
68
 
69
69
  type RemoveRouteGroups<T extends string> =
70
- T extends `${infer Before}(${infer RouteGroup})${infer After}`
70
+ T extends `${infer Before}(${string})${infer After}`
71
71
  ? RemoveRouteGroups<`${Before}${After}`>
72
72
  : T
73
73
 
@@ -76,32 +76,22 @@ type NormalizeSlashes<T extends string> =
76
76
  ? NormalizeSlashes<`${Before}/${After}`>
77
77
  : T
78
78
 
79
- type ReplaceFirstOccurrence<
80
- TValue extends string,
81
- TSearch extends string,
82
- TReplacement extends string,
83
- > = TValue extends `${infer Prefix}${TSearch}${infer Suffix}`
84
- ? `${Prefix}${TReplacement}${Suffix}`
85
- : TValue
86
-
87
79
  export type ResolveFilePath<
88
80
  TParentRoute extends AnyRoute,
89
81
  TFilePath extends string,
90
82
  > = TParentRoute['id'] extends RootRouteId
91
83
  ? TrimPathLeft<TFilePath>
92
- : ReplaceFirstOccurrence<
93
- TrimPathLeft<TFilePath>,
94
- TrimPathLeft<TParentRoute['types']['customId']>,
95
- ''
96
- >
84
+ : TFilePath extends `${TParentRoute['types']['customId']}${infer TRest}`
85
+ ? TRest
86
+ : TFilePath
97
87
 
98
88
  export type FileRoutePath<
99
89
  TParentRoute extends AnyRoute,
100
90
  TFilePath extends string,
101
91
  TResolvedFilePath = ResolveFilePath<TParentRoute, TFilePath>,
102
- > = TResolvedFilePath extends `_${infer _}`
92
+ > = TResolvedFilePath extends `_${string}`
103
93
  ? ''
104
- : TResolvedFilePath extends `/_${infer _}`
94
+ : TResolvedFilePath extends `/_${string}`
105
95
  ? ''
106
96
  : TResolvedFilePath
107
97
 
@@ -154,34 +144,27 @@ export class FileRoute<
154
144
  createRoute = <
155
145
  TSearchSchemaInput extends RouteConstraints['TSearchSchema'] = {},
156
146
  TSearchSchema extends RouteConstraints['TSearchSchema'] = {},
157
- TSearchSchemaUsed extends Record<
158
- string,
159
- any
160
- > = TSearchSchemaInput extends SearchSchemaInput
147
+ TSearchSchemaUsed = TSearchSchemaInput extends SearchSchemaInput
161
148
  ? Omit<TSearchSchemaInput, keyof SearchSchemaInput>
162
149
  : TSearchSchema,
163
- TFullSearchSchemaInput extends
164
- RouteConstraints['TFullSearchSchema'] = ResolveFullSearchSchemaInput<
150
+ TFullSearchSchemaInput = ResolveFullSearchSchemaInput<
165
151
  TParentRoute,
166
152
  TSearchSchemaUsed
167
153
  >,
168
154
  TFullSearchSchema = ResolveFullSearchSchema<TParentRoute, TSearchSchema>,
169
- TParams extends RouteConstraints['TParams'] = Expand<
170
- Record<ParsePathParams<TPath>, string>
171
- >,
172
- TAllParams extends RouteConstraints['TAllParams'] = MergeFromFromParent<
155
+ TParams = Record<ParsePathParams<TPath>, string>,
156
+ TAllParams = MergeFromFromParent<
173
157
  TParentRoute['types']['allParams'],
174
158
  TParams
175
159
  >,
176
160
  TRouteContextReturn extends
177
161
  RouteConstraints['TRouteContext'] = RouteContext,
178
- TRouteContext extends RouteConstraints['TRouteContext'] = [
179
- TRouteContextReturn,
180
- ] extends [never]
162
+ TRouteContext = [TRouteContextReturn] extends [never]
181
163
  ? RouteContext
182
164
  : TRouteContextReturn,
183
- TAllContext = Expand<
184
- Assign<IsAny<TParentRoute['types']['allContext'], {}>, TRouteContext>
165
+ TAllContext = Assign<
166
+ IsAny<TParentRoute['types']['allContext'], {}>,
167
+ TRouteContext
185
168
  >,
186
169
  TRouterContext extends RouteConstraints['TRouterContext'] = AnyContext,
187
170
  TLoaderDeps extends Record<string, any> = {},
@@ -190,7 +173,6 @@ export class FileRoute<
190
173
  ? undefined
191
174
  : TLoaderDataReturn,
192
175
  TChildren extends RouteConstraints['TChildren'] = unknown,
193
- TRouteTree extends RouteConstraints['TRouteTree'] = AnyRoute,
194
176
  >(
195
177
  options?: FileBaseRouteOptions<
196
178
  TParentRoute,
@@ -228,8 +210,7 @@ export class FileRoute<
228
210
  TLoaderDeps,
229
211
  TLoaderDataReturn,
230
212
  TLoaderData,
231
- TChildren,
232
- TRouteTree
213
+ TChildren
233
214
  > => {
234
215
  warning(
235
216
  this.silent,
@@ -294,7 +275,7 @@ export class LazyRoute<TRoute extends AnyRoute> {
294
275
 
295
276
  useMatch = <
296
277
  TRouteMatchState = RouteMatch<
297
- TRoute['types']['routeTree'],
278
+ RegisteredRouter['routeTree'],
298
279
  TRoute['types']['id']
299
280
  >,
300
281
  TSelected = TRouteMatchState,
package/src/index.tsx CHANGED
@@ -67,7 +67,6 @@ export {
67
67
  type ActiveOptions,
68
68
  type LinkOptions,
69
69
  type CheckPath,
70
- type CheckPathError,
71
70
  type ResolveRelativePath,
72
71
  type UseLinkPropsOptions,
73
72
  type ActiveLinkOptions,
@@ -90,7 +89,6 @@ export {
90
89
  useLoaderData,
91
90
  isServerSideError,
92
91
  defaultDeserializeError,
93
- getRenderedMatches,
94
92
  type RouteMatch,
95
93
  type AnyRouteMatch,
96
94
  type MatchRouteOptions,