@tanstack/router-core 0.0.1-beta.11 → 0.0.1-beta.14

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/router-core",
3
3
  "author": "Tanner Linsley",
4
- "version": "0.0.1-beta.11",
4
+ "version": "0.0.1-beta.14",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://tanstack.com/router",
package/src/frameworks.ts CHANGED
@@ -3,8 +3,7 @@ export interface FrameworkGenerics {
3
3
  // and are extended by framework adapters, but cannot be
4
4
  // pre-defined as constraints:
5
5
  //
6
- // Element: any
7
- // SyncOrAsyncElement?: any
6
+ // Component: any
8
7
  }
9
8
 
10
9
  export type GetFrameworkGeneric<U> = U extends keyof FrameworkGenerics
package/src/index.ts CHANGED
@@ -7,7 +7,6 @@ export {
7
7
  export { default as invariant } from 'tiny-invariant'
8
8
 
9
9
  export * from './frameworks'
10
- export * from './index'
11
10
  export * from './link'
12
11
  export * from './path'
13
12
  export * from './qss'
@@ -108,18 +108,12 @@ export type RouteOptions<
108
108
  // Filter functions that can manipulate search params *after* they are passed to links and navigate
109
109
  // calls that match this route.
110
110
  postSearchFilters?: SearchFilter<TFullSearchSchema>[]
111
- // The duration to wait during `loader` execution before showing the `pendingElement`
112
- pendingMs?: number
113
- // _If the `pendingElement` is shown_, the minimum duration for which it will be visible.
114
- pendingMinMs?: number
115
- // The content to be rendered when the route is matched. If no element is provided, defaults to `<Outlet />`
116
- element?: GetFrameworkGeneric<'SyncOrAsyncElement'> // , NoInfer<TLoaderData>>
117
- // The content to be rendered when `loader` encounters an error
118
- errorElement?: GetFrameworkGeneric<'SyncOrAsyncElement'> // , NoInfer<TLoaderData>>
119
- // The content to be rendered when rendering encounters an error
120
- catchElement?: GetFrameworkGeneric<'SyncOrAsyncElement'> // , NoInfer<TLoaderData>>
121
- // The content to be rendered when the duration of `loader` execution surpasses the `pendingMs` duration
122
- pendingElement?: GetFrameworkGeneric<'SyncOrAsyncElement'> //, NoInfer<TLoaderData>>
111
+ // The content to be rendered when the route is matched. If no component is provided, defaults to `<Outlet />`
112
+ component?: GetFrameworkGeneric<'Component'> // , NoInfer<TLoaderData>>
113
+ // The content to be rendered when the route encounters an error
114
+ errorComponent?: GetFrameworkGeneric<'Component'> // , NoInfer<TLoaderData>>
115
+ // If supported by your framework, the content to be rendered as the fallback content until the route is ready to render
116
+ pendingComponent?: GetFrameworkGeneric<'Component'> //, NoInfer<TLoaderData>>
123
117
  // An asynchronous function responsible for preparing or fetching data for the route before it is rendered
124
118
  loader?: LoaderFn<TRouteLoaderData, TFullSearchSchema, TAllParams>
125
119
  // The max age to consider loader data fresh (not-stale) for this route in milliseconds from the time of fetch
@@ -132,7 +126,7 @@ export type RouteOptions<
132
126
  // might invalidate the route's data.
133
127
  action?: ActionFn<TActionPayload, TActionResponse>
134
128
  // Set this to true to rethrow errors up the component tree to either the nearest error boundary or
135
- // route with error element, whichever comes first.
129
+ // route with error component, whichever comes first.
136
130
  useErrorBoundary?: boolean
137
131
  // This function is called
138
132
  // when moving from an inactive state to an active one. Likewise, when moving from
package/src/routeMatch.ts CHANGED
@@ -28,20 +28,14 @@ export interface RouteMatch<
28
28
  loaderData: TRouteInfo['loaderData']
29
29
  routeLoaderData: TRouteInfo['routeLoaderData']
30
30
  isFetching: boolean
31
- isPending: boolean
32
31
  invalidAt: number
33
32
  __: {
34
- element?: GetFrameworkGeneric<'Element'> // , TRouteInfo['loaderData']>
35
- errorElement?: GetFrameworkGeneric<'Element'> // , TRouteInfo['loaderData']>
36
- catchElement?: GetFrameworkGeneric<'Element'> // , TRouteInfo['loaderData']>
37
- pendingElement?: GetFrameworkGeneric<'Element'> // , TRouteInfo['loaderData']>
33
+ component?: GetFrameworkGeneric<'Component'> // , TRouteInfo['loaderData']>
34
+ errorComponent?: GetFrameworkGeneric<'Component'> // , TRouteInfo['loaderData']>
35
+ pendingComponent?: GetFrameworkGeneric<'Component'> // , TRouteInfo['loaderData']>
38
36
  loadPromise?: Promise<void>
39
- loaderDataPromise?: Promise<void>
40
- elementsPromise?: Promise<void>
37
+ componentsPromise?: Promise<void>
41
38
  dataPromise?: Promise<void>
42
- pendingTimeout?: Timeout
43
- pendingMinTimeout?: Timeout
44
- pendingMinPromise?: Promise<void>
45
39
  onExit?:
46
40
  | void
47
41
  | ((matchContext: {
@@ -53,28 +47,24 @@ export interface RouteMatch<
53
47
  // setParentMatch: (parentMatch: RouteMatch) => void
54
48
  // addChildMatch: (childMatch: RouteMatch) => void
55
49
  validate: () => void
56
- startPending: () => void
57
- cancelPending: () => void
58
50
  notify: () => void
59
51
  resolve: () => void
60
52
  }
61
53
  cancel: () => void
62
54
  load: (
63
- loaderOpts?: { withPending?: boolean } & (
55
+ loaderOpts?:
64
56
  | { preload: true; maxAge: number; gcMaxAge: number }
65
- | { preload?: false; maxAge?: never; gcMaxAge?: never }
66
- ),
57
+ | { preload?: false; maxAge?: never; gcMaxAge?: never },
67
58
  ) => Promise<TRouteInfo['routeLoaderData']>
68
59
  fetch: (opts?: { maxAge?: number }) => Promise<TRouteInfo['routeLoaderData']>
69
60
  invalidate: () => void
70
61
  hasLoaders: () => boolean
71
62
  }
72
63
 
73
- const elementTypes = [
74
- 'element',
75
- 'errorElement',
76
- 'catchElement',
77
- 'pendingElement',
64
+ const componentTypes = [
65
+ 'component',
66
+ 'errorComponent',
67
+ 'pendingComponent',
78
68
  ] as const
79
69
 
80
70
  export function createRouteMatch<
@@ -99,7 +89,6 @@ export function createRouteMatch<
99
89
  status: 'idle',
100
90
  routeLoaderData: {} as TRouteInfo['routeLoaderData'],
101
91
  loaderData: {} as TRouteInfo['loaderData'],
102
- isPending: false,
103
92
  isFetching: false,
104
93
  isInvalid: false,
105
94
  invalidAt: Infinity,
@@ -116,37 +105,6 @@ export function createRouteMatch<
116
105
  routeMatch.__.resolve()
117
106
  routeMatch.router.notify()
118
107
  },
119
- startPending: () => {
120
- const pendingMs =
121
- routeMatch.options.pendingMs ?? router.options.defaultPendingMs
122
- const pendingMinMs =
123
- routeMatch.options.pendingMinMs ?? router.options.defaultPendingMinMs
124
-
125
- if (
126
- routeMatch.__.pendingTimeout ||
127
- routeMatch.status !== 'loading' ||
128
- typeof pendingMs === 'undefined'
129
- ) {
130
- return
131
- }
132
-
133
- routeMatch.__.pendingTimeout = setTimeout(() => {
134
- routeMatch.isPending = true
135
- routeMatch.__.resolve()
136
- if (typeof pendingMinMs !== 'undefined') {
137
- routeMatch.__.pendingMinPromise = new Promise(
138
- (r) =>
139
- (routeMatch.__.pendingMinTimeout = setTimeout(r, pendingMinMs)),
140
- )
141
- }
142
- }, pendingMs)
143
- },
144
- cancelPending: () => {
145
- routeMatch.isPending = false
146
- clearTimeout(routeMatch.__.pendingTimeout)
147
- clearTimeout(routeMatch.__.pendingMinTimeout)
148
- delete routeMatch.__.pendingMinPromise
149
- },
150
108
  validate: () => {
151
109
  // Validate the search params and stabilize them
152
110
  const parentSearch =
@@ -177,11 +135,11 @@ export function createRouteMatch<
177
135
  ...nextSearch,
178
136
  })
179
137
 
180
- elementTypes.map(async (type) => {
181
- const routeElement = routeMatch.options[type]
138
+ componentTypes.map(async (type) => {
139
+ const component = routeMatch.options[type]
182
140
 
183
141
  if (typeof routeMatch.__[type] !== 'function') {
184
- routeMatch.__[type] = routeElement
142
+ routeMatch.__[type] = component
185
143
  }
186
144
  })
187
145
  } catch (err: any) {
@@ -199,7 +157,6 @@ export function createRouteMatch<
199
157
  },
200
158
  cancel: () => {
201
159
  routeMatch.__.abortController?.abort()
202
- routeMatch.__.cancelPending()
203
160
  },
204
161
  invalidate: () => {
205
162
  routeMatch.isInvalid = true
@@ -207,7 +164,7 @@ export function createRouteMatch<
207
164
  hasLoaders: () => {
208
165
  return !!(
209
166
  route.options.loader ||
210
- elementTypes.some((d) => typeof route.options[d] === 'function')
167
+ componentTypes.some((d) => route.options[d]?.preload)
211
168
  )
212
169
  },
213
170
  load: async (loaderOpts) => {
@@ -262,102 +219,87 @@ export function createRouteMatch<
262
219
  routeMatch.isFetching = true
263
220
  routeMatch.__.resolve = resolve as () => void
264
221
 
265
- routeMatch.__.loaderDataPromise = (async () => {
266
- // Load the elements and data in parallel
267
-
268
- routeMatch.__.elementsPromise = (async () => {
269
- // then run all element and data loaders in parallel
270
- // For each element type, potentially load it asynchronously
271
-
272
- await Promise.all(
273
- elementTypes.map(async (type) => {
274
- const routeElement = routeMatch.options[type]
275
-
276
- if (typeof routeMatch.__[type] === 'function') {
277
- routeMatch.__[type] = await router.options.createElement!(
278
- routeElement,
279
- )
280
- }
281
- }),
282
- )
283
- })()
222
+ routeMatch.__.componentsPromise = (async () => {
223
+ // then run all component and data loaders in parallel
224
+ // For each component type, potentially load it asynchronously
284
225
 
285
- routeMatch.__.dataPromise = Promise.resolve().then(async () => {
286
- try {
287
- if (routeMatch.options.loader) {
288
- const data = await routeMatch.options.loader({
289
- params: routeMatch.params,
290
- search: routeMatch.routeSearch,
291
- signal: routeMatch.__.abortController.signal,
292
- })
293
- if (id !== routeMatch.__.latestId) {
294
- return routeMatch.__.loadPromise
295
- }
226
+ await Promise.all(
227
+ componentTypes.map(async (type) => {
228
+ const component = routeMatch.options[type]
296
229
 
297
- routeMatch.routeLoaderData = replaceEqualDeep(
298
- routeMatch.routeLoaderData,
299
- data,
230
+ if (routeMatch.__[type]?.preload) {
231
+ routeMatch.__[type] = await router.options.loadComponent!(
232
+ component,
300
233
  )
301
234
  }
235
+ }),
236
+ )
237
+ })()
302
238
 
303
- routeMatch.error = undefined
304
- routeMatch.status = 'success'
305
- routeMatch.updatedAt = Date.now()
306
- routeMatch.invalidAt =
307
- routeMatch.updatedAt +
308
- (opts?.maxAge ??
309
- routeMatch.options.loaderMaxAge ??
310
- router.options.defaultLoaderMaxAge ??
311
- 0)
312
- } catch (err) {
239
+ routeMatch.__.dataPromise = Promise.resolve().then(async () => {
240
+ try {
241
+ if (routeMatch.options.loader) {
242
+ const data = await routeMatch.options.loader({
243
+ params: routeMatch.params,
244
+ search: routeMatch.routeSearch,
245
+ signal: routeMatch.__.abortController.signal,
246
+ })
313
247
  if (id !== routeMatch.__.latestId) {
314
248
  return routeMatch.__.loadPromise
315
249
  }
316
250
 
317
- if (process.env.NODE_ENV !== 'production') {
318
- console.error(err)
319
- }
320
- routeMatch.error = err
321
- routeMatch.status = 'error'
322
- routeMatch.updatedAt = Date.now()
251
+ routeMatch.routeLoaderData = replaceEqualDeep(
252
+ routeMatch.routeLoaderData,
253
+ data,
254
+ )
323
255
  }
324
- })
325
256
 
326
- try {
327
- await Promise.all([
328
- routeMatch.__.elementsPromise,
329
- routeMatch.__.dataPromise,
330
- ])
257
+ routeMatch.error = undefined
258
+ routeMatch.status = 'success'
259
+ routeMatch.updatedAt = Date.now()
260
+ routeMatch.invalidAt =
261
+ routeMatch.updatedAt +
262
+ (opts?.maxAge ??
263
+ routeMatch.options.loaderMaxAge ??
264
+ router.options.defaultLoaderMaxAge ??
265
+ 0)
266
+ } catch (err) {
331
267
  if (id !== routeMatch.__.latestId) {
332
268
  return routeMatch.__.loadPromise
333
269
  }
334
270
 
335
- if (routeMatch.__.pendingMinPromise) {
336
- await routeMatch.__.pendingMinPromise
337
- delete routeMatch.__.pendingMinPromise
271
+ if (process.env.NODE_ENV !== 'production') {
272
+ console.error(err)
338
273
  }
339
- } finally {
340
- if (id !== routeMatch.__.latestId) {
341
- return routeMatch.__.loadPromise
342
- }
343
- routeMatch.__.cancelPending()
344
- routeMatch.isPending = false
345
- routeMatch.isFetching = false
346
- routeMatch.__.notify()
274
+ routeMatch.error = err
275
+ routeMatch.status = 'error'
276
+ routeMatch.updatedAt = Date.now()
347
277
  }
348
- })()
278
+ })
349
279
 
350
- await routeMatch.__.loaderDataPromise
351
-
352
- if (id !== routeMatch.__.latestId) {
353
- return routeMatch.__.loadPromise
280
+ try {
281
+ await Promise.all([
282
+ routeMatch.__.componentsPromise,
283
+ routeMatch.__.dataPromise,
284
+ ])
285
+ if (id !== routeMatch.__.latestId) {
286
+ return routeMatch.__.loadPromise
287
+ }
288
+ } finally {
289
+ if (id !== routeMatch.__.latestId) {
290
+ return routeMatch.__.loadPromise
291
+ }
292
+ routeMatch.isFetching = false
293
+ routeMatch.__.notify()
354
294
  }
355
-
356
- delete routeMatch.__.loaderDataPromise
357
295
  })
358
296
 
359
297
  await routeMatch.__.loadPromise
360
298
 
299
+ if (id !== routeMatch.__.latestId) {
300
+ return routeMatch.__.loadPromise
301
+ }
302
+
361
303
  delete routeMatch.__.loadPromise
362
304
  },
363
305
  }
package/src/router.ts CHANGED
@@ -6,6 +6,7 @@ import {
6
6
  History,
7
7
  MemoryHistory,
8
8
  } from 'history'
9
+ import React from 'react'
9
10
  import invariant from 'tiny-invariant'
10
11
  import { GetFrameworkGeneric } from './frameworks'
11
12
 
@@ -91,12 +92,9 @@ export interface RouterOptions<TRouteConfig extends AnyRouteConfig> {
91
92
  defaultPreloadGcMaxAge?: number
92
93
  defaultPreloadDelay?: number
93
94
  useErrorBoundary?: boolean
94
- defaultElement?: GetFrameworkGeneric<'Element'>
95
- defaultErrorElement?: GetFrameworkGeneric<'Element'>
96
- defaultCatchElement?: GetFrameworkGeneric<'Element'>
97
- defaultPendingElement?: GetFrameworkGeneric<'Element'>
98
- defaultPendingMs?: number
99
- defaultPendingMinMs?: number
95
+ defaultComponent?: GetFrameworkGeneric<'Component'>
96
+ defaultErrorComponent?: GetFrameworkGeneric<'Component'>
97
+ defaultPendingComponent?: GetFrameworkGeneric<'Component'>
100
98
  defaultLoaderMaxAge?: number
101
99
  defaultLoaderGcMaxAge?: number
102
100
  caseSensitive?: boolean
@@ -104,9 +102,12 @@ export interface RouterOptions<TRouteConfig extends AnyRouteConfig> {
104
102
  basepath?: string
105
103
  createRouter?: (router: Router<any, any>) => void
106
104
  createRoute?: (opts: { route: AnyRoute; router: Router<any, any> }) => void
107
- createElement?: (
108
- element: GetFrameworkGeneric<'SyncOrAsyncElement'>,
109
- ) => Promise<GetFrameworkGeneric<'Element'>>
105
+ loadComponent?: (
106
+ component: GetFrameworkGeneric<'Component'>,
107
+ ) => Promise<GetFrameworkGeneric<'Component'>>
108
+ // renderComponent?: (
109
+ // component: GetFrameworkGeneric<'Component'>,
110
+ // ) => GetFrameworkGeneric<'Element'>
110
111
  }
111
112
 
112
113
  export interface Action<
@@ -294,10 +295,9 @@ export interface Router<
294
295
  ) => RouteMatch[]
295
296
  loadMatches: (
296
297
  resolvedMatches: RouteMatch[],
297
- loaderOpts?: { withPending?: boolean } & (
298
+ loaderOpts?:
298
299
  | { preload: true; maxAge: number; gcMaxAge: number }
299
- | { preload?: false; maxAge?: never; gcMaxAge?: never }
300
- ),
300
+ | { preload?: false; maxAge?: never; gcMaxAge?: never },
301
301
  ) => Promise<void>
302
302
  invalidateRoute: (opts: MatchLocation) => void
303
303
  reload: () => Promise<void>
@@ -408,16 +408,25 @@ export function createRouter<
408
408
  return router.routesById[id]
409
409
  },
410
410
  notify: (): void => {
411
- router.state = {
412
- ...router.state,
413
- isFetching:
414
- router.state.status === 'loading' ||
415
- router.state.matches.some((d) => d.isFetching),
416
- isPreloading: Object.values(router.matchCache).some(
417
- (d) =>
418
- d.match.isFetching &&
419
- !router.state.matches.find((dd) => dd.matchId === d.match.matchId),
420
- ),
411
+ const isFetching =
412
+ router.state.status === 'loading' ||
413
+ router.state.matches.some((d) => d.isFetching)
414
+
415
+ const isPreloading = Object.values(router.matchCache).some(
416
+ (d) =>
417
+ d.match.isFetching &&
418
+ !router.state.matches.find((dd) => dd.matchId === d.match.matchId),
419
+ )
420
+
421
+ if (
422
+ router.state.isFetching !== isFetching ||
423
+ router.state.isPreloading !== isPreloading
424
+ ) {
425
+ router.state = {
426
+ ...router.state,
427
+ isFetching,
428
+ isPreloading,
429
+ }
421
430
  }
422
431
 
423
432
  cascadeLoaderData(router.state.matches)
@@ -455,7 +464,7 @@ export function createRouter<
455
464
  Object.assign(match, dehydratedMatch)
456
465
  })
457
466
 
458
- router.loadMatches(matches)
467
+ matches.forEach((match) => match.__.validate())
459
468
 
460
469
  router.state = {
461
470
  ...router.state,
@@ -477,7 +486,9 @@ export function createRouter<
477
486
  router.__.commitLocation(next, true)
478
487
  }
479
488
 
480
- // router.load()
489
+ if (!router.state.matches.length) {
490
+ router.load()
491
+ }
481
492
 
482
493
  const unsub = router.history.listen((event) => {
483
494
  router.load(router.__.parseLocation(event.location, router.location))
@@ -555,21 +566,28 @@ export function createRouter<
555
566
  strictParseParams: true,
556
567
  })
557
568
 
558
- router.state = {
559
- ...router.state,
560
- pending: {
569
+ if (typeof document !== 'undefined') {
570
+ router.state = {
571
+ ...router.state,
572
+ pending: {
573
+ matches: matches,
574
+ location: router.location,
575
+ },
576
+ status: 'loading',
577
+ }
578
+ } else {
579
+ router.state = {
580
+ ...router.state,
561
581
  matches: matches,
562
582
  location: router.location,
563
- },
564
- status: 'loading',
583
+ status: 'loading',
584
+ }
565
585
  }
566
586
 
567
587
  router.notify()
568
588
 
569
589
  // Load the matches
570
- await router.loadMatches(matches, {
571
- withPending: true,
572
- })
590
+ await router.loadMatches(matches)
573
591
 
574
592
  if (router.startedLoadingAt !== id) {
575
593
  // Ignore side-effects of match loading
@@ -821,14 +839,8 @@ export function createRouter<
821
839
  match.__.validate()
822
840
  match.load(loaderOpts)
823
841
 
824
- if (match.status === 'loading') {
825
- // If requested, start the pending timers
826
- if (loaderOpts?.withPending) match.__.startPending()
827
- }
828
-
829
842
  if (match.__.loadPromise) {
830
843
  // Wait for the first sign of activity from the match
831
- // This might be completion, error, or a pending state
832
844
  await match.__.loadPromise
833
845
  }
834
846
  })