@tanstack/react-router 0.0.1-beta.2 → 0.0.1-beta.20

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/react-router",
3
3
  "author": "Tanner Linsley",
4
- "version": "0.0.1-beta.2",
4
+ "version": "0.0.1-beta.20",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://tanstack.com/router/",
@@ -34,13 +34,14 @@
34
34
  "build/**",
35
35
  "src"
36
36
  ],
37
+ "sideEffects": false,
37
38
  "peerDependencies": {
38
39
  "react": ">=16",
39
40
  "react-dom": ">=16"
40
41
  },
41
42
  "dependencies": {
42
43
  "@babel/runtime": "^7.16.7",
43
- "@tanstack/router-core": "0.0.1-beta.2",
44
+ "@tanstack/router-core": "0.0.1-beta.19",
44
45
  "use-sync-external-store": "^1.2.0"
45
46
  },
46
47
  "devDependencies": {
package/src/index.tsx CHANGED
@@ -6,7 +6,6 @@ import {
6
6
  AnyRoute,
7
7
  CheckId,
8
8
  rootRouteId,
9
- Router,
10
9
  RouterState,
11
10
  ToIdOption,
12
11
  } from '@tanstack/router-core'
@@ -31,15 +30,67 @@ import {
31
30
  NoInfer,
32
31
  ToOptions,
33
32
  invariant,
33
+ Router,
34
34
  } from '@tanstack/router-core'
35
35
 
36
36
  export * from '@tanstack/router-core'
37
37
 
38
+ export interface RegisterRouter {
39
+ // router: Router
40
+ }
41
+
42
+ export type ResolvedRouter = RegisterRouter extends {
43
+ router: Router<infer TRouteConfig, infer TAllRouteInfo>
44
+ }
45
+ ? Router<TRouteConfig, TAllRouteInfo>
46
+ : Router
47
+
48
+ export type ResolvedAllRouteInfo = RegisterRouter extends {
49
+ router: Router<infer TRouteConfig, infer TAllRouteInfo>
50
+ }
51
+ ? TAllRouteInfo
52
+ : AnyAllRouteInfo
53
+
54
+ export type SyncRouteComponent = (props?: {}) => JSX.Element | React.ReactNode
55
+
56
+ export type RouteComponent = SyncRouteComponent & {
57
+ preload?: () => Promise<SyncRouteComponent>
58
+ }
59
+
60
+ export function lazy(
61
+ importer: () => Promise<{ default: SyncRouteComponent }>,
62
+ ): RouteComponent {
63
+ const lazyComp = React.lazy(importer as any)
64
+ let promise: Promise<SyncRouteComponent>
65
+ let resolvedComp: SyncRouteComponent
66
+
67
+ const forwardedComp = React.forwardRef((props, ref) => {
68
+ const resolvedCompRef = React.useRef(resolvedComp || lazyComp)
69
+ return React.createElement(
70
+ resolvedCompRef.current as any,
71
+ { ...(ref ? { ref } : {}), ...props } as any,
72
+ )
73
+ })
74
+
75
+ const finalComp = forwardedComp as unknown as RouteComponent
76
+
77
+ finalComp.preload = () => {
78
+ if (!promise) {
79
+ promise = importer().then((module) => {
80
+ resolvedComp = module.default
81
+ return resolvedComp
82
+ })
83
+ }
84
+
85
+ return promise
86
+ }
87
+
88
+ return finalComp
89
+ }
90
+
38
91
  declare module '@tanstack/router-core' {
39
92
  interface FrameworkGenerics {
40
- Element: React.ReactNode
41
- // Any is required here so import() will work without having to do import().then(d => d.default)
42
- SyncOrAsyncElement: React.ReactNode | (() => Promise<any>)
93
+ Component: RouteComponent
43
94
  }
44
95
 
45
96
  interface Router<
@@ -50,9 +101,17 @@ declare module '@tanstack/router-core' {
50
101
  useRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
51
102
  routeId: TId,
52
103
  ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
53
- useMatch: <TId extends keyof TAllRouteInfo['routeInfoById']>(
104
+ useMatch: <
105
+ TId extends keyof TAllRouteInfo['routeInfoById'],
106
+ TStrict extends true | false = true,
107
+ >(
54
108
  routeId: TId,
55
- ) => RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
109
+ opts?: { strict?: TStrict },
110
+ ) => TStrict extends true
111
+ ? RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
112
+ :
113
+ | RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
114
+ | undefined
56
115
  linkProps: <TTo extends string = '.'>(
57
116
  props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
58
117
  React.AnchorHTMLAttributes<HTMLAnchorElement>,
@@ -99,6 +158,7 @@ declare module '@tanstack/router-core' {
99
158
  TResolved,
100
159
  ToIdOption<TAllRouteInfo, TRouteInfo['id'], TTo>
101
160
  >,
161
+ opts?: { strict?: boolean },
102
162
  ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TResolved]>
103
163
  linkProps: <TTo extends string = '.'>(
104
164
  props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
@@ -154,18 +214,24 @@ export type PromptProps = {
154
214
 
155
215
  //
156
216
 
157
- const matchesContext = React.createContext<RouteMatch[]>(null!)
158
- const routerContext = React.createContext<{ router: Router<any, any> }>(null!)
217
+ export function Link<TTo extends string = '.'>(
218
+ props: LinkPropsOptions<ResolvedAllRouteInfo, '/', TTo> &
219
+ React.AnchorHTMLAttributes<HTMLAnchorElement> &
220
+ Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
221
+ children?:
222
+ | React.ReactNode
223
+ | ((state: { isActive: boolean }) => React.ReactNode)
224
+ },
225
+ ): JSX.Element {
226
+ const router = useRouter()
227
+ return <router.Link {...(props as any)} />
228
+ }
159
229
 
160
- // Detect if we're in the DOM
161
- const isDOM = Boolean(
162
- typeof window !== 'undefined' &&
163
- window.document &&
164
- window.document.createElement,
230
+ export const matchesContext = React.createContext<RouteMatch[]>(null!)
231
+ export const routerContext = React.createContext<{ router: ResolvedRouter }>(
232
+ null!,
165
233
  )
166
234
 
167
- const useLayoutEffect = isDOM ? React.useLayoutEffect : React.useEffect
168
-
169
235
  export type MatchesProviderProps = {
170
236
  value: RouteMatch[]
171
237
  children: React.ReactNode
@@ -253,6 +319,12 @@ export function createReactRouter<
253
319
  next,
254
320
  } = linkInfo
255
321
 
322
+ const reactHandleClick = (e: Event) => {
323
+ React.startTransition(() => {
324
+ handleClick(e)
325
+ })
326
+ }
327
+
256
328
  const composeHandlers =
257
329
  (handlers: (undefined | ((e: any) => void))[]) =>
258
330
  (e: React.SyntheticEvent) => {
@@ -275,7 +347,7 @@ export function createReactRouter<
275
347
  ...resolvedInactiveProps,
276
348
  ...rest,
277
349
  href: disabled ? undefined : next.href,
278
- onClick: composeHandlers([handleClick, onClick]),
350
+ onClick: composeHandlers([reactHandleClick, onClick]),
279
351
  onFocus: composeHandlers([handleFocus, onFocus]),
280
352
  onMouseEnter: composeHandlers([handleEnter, onMouseEnter]),
281
353
  onMouseLeave: composeHandlers([handleLeave, onMouseLeave]),
@@ -349,7 +421,7 @@ export function createReactRouter<
349
421
  useRouterSubscription(router)
350
422
  return router.state
351
423
  },
352
- useMatch: (routeId) => {
424
+ useMatch: (routeId, opts) => {
353
425
  useRouterSubscription(router)
354
426
 
355
427
  invariant(
@@ -357,32 +429,30 @@ export function createReactRouter<
357
429
  `"${rootRouteId}" cannot be used with useMatch! Did you mean to useRoute("${rootRouteId}")?`,
358
430
  )
359
431
 
360
- const runtimeMatch = useMatch()
432
+ const runtimeMatch = useMatches()?.[0]!
361
433
  const match = router.state.matches.find((d) => d.routeId === routeId)
362
434
 
363
- invariant(
364
- match,
365
- `Could not find a match for route "${
366
- routeId as string
367
- }" being rendered in this component!`,
368
- )
369
-
370
- invariant(
371
- runtimeMatch.routeId == match?.routeId,
372
- `useMatch('${
373
- match?.routeId as string
374
- }') is being called in a component that is meant to render the '${
375
- runtimeMatch.routeId
376
- }' route. Did you mean to 'useRoute(${
377
- match?.routeId as string
378
- })' instead?`,
379
- )
380
-
381
- if (!match) {
382
- invariant('Match not found!')
435
+ if (opts?.strict ?? true) {
436
+ invariant(
437
+ match,
438
+ `Could not find an active match for "${routeId as string}"!`,
439
+ )
440
+
441
+ invariant(
442
+ runtimeMatch.routeId == match?.routeId,
443
+ `useMatch("${
444
+ match?.routeId as string
445
+ }") is being called in a component that is meant to render the '${
446
+ runtimeMatch.routeId
447
+ }' route. Did you mean to 'useMatch("${
448
+ match?.routeId as string
449
+ }", { strict: false })' or 'useRoute("${
450
+ match?.routeId as string
451
+ }")' instead?`,
452
+ )
383
453
  }
384
454
 
385
- return match
455
+ return match as any
386
456
  },
387
457
  }
388
458
 
@@ -395,19 +465,13 @@ export function createReactRouter<
395
465
 
396
466
  Object.assign(route, routeExt)
397
467
  },
398
- createElement: async (element) => {
399
- if (typeof element === 'function') {
400
- const res = (await element()) as any
401
-
402
- // Support direct import() calls
403
- if (typeof res === 'object' && res.default) {
404
- return React.createElement(res.default)
405
- } else {
406
- return res
407
- }
468
+ loadComponent: async (component) => {
469
+ if (component.preload && typeof document !== 'undefined') {
470
+ component.preload()
471
+ // return await component.preload()
408
472
  }
409
473
 
410
- return element
474
+ return component as any
411
475
  },
412
476
  })
413
477
 
@@ -430,13 +494,13 @@ export function RouterProvider<
430
494
  router.update(rest)
431
495
 
432
496
  useRouterSubscription(router)
433
-
434
- useLayoutEffect(() => {
497
+ React.useEffect(() => {
498
+ console.log('hello')
435
499
  return router.mount()
436
500
  }, [router])
437
501
 
438
502
  return (
439
- <routerContext.Provider value={{ router }}>
503
+ <routerContext.Provider value={{ router: router as any }}>
440
504
  <MatchesProvider value={router.state.matches}>
441
505
  {children ?? <Outlet />}
442
506
  </MatchesProvider>
@@ -444,93 +508,86 @@ export function RouterProvider<
444
508
  )
445
509
  }
446
510
 
447
- function useRouter(): Router {
511
+ export function useRouter(): ResolvedRouter {
448
512
  const value = React.useContext(routerContext)
449
513
  warning(!value, 'useRouter must be used inside a <Router> component!')
450
514
 
451
515
  useRouterSubscription(value.router)
452
516
 
453
- return value.router as Router
517
+ return value.router
454
518
  }
455
519
 
456
- function useMatches(): RouteMatch[] {
520
+ export function useMatches(): RouteMatch[] {
457
521
  return React.useContext(matchesContext)
458
522
  }
459
523
 
460
- // function useParentMatches(): RouteMatch[] {
461
- // const router = useRouter()
462
- // const match = useMatch()
463
- // const matches = router.state.matches
464
- // return matches.slice(
465
- // 0,
466
- // matches.findIndex((d) => d.matchId === match.matchId) - 1,
467
- // )
468
- // }
469
-
470
- function useMatch<T>(): RouteMatch {
471
- return useMatches()?.[0] as RouteMatch
524
+ export function useMatch<
525
+ TId extends keyof ResolvedAllRouteInfo['routeInfoById'],
526
+ TStrict extends true | false = true,
527
+ >(
528
+ routeId: TId,
529
+ opts?: { strict?: TStrict },
530
+ ): TStrict extends true
531
+ ? RouteMatch<ResolvedAllRouteInfo, ResolvedAllRouteInfo['routeInfoById'][TId]>
532
+ :
533
+ | RouteMatch<
534
+ ResolvedAllRouteInfo,
535
+ ResolvedAllRouteInfo['routeInfoById'][TId]
536
+ >
537
+ | undefined {
538
+ const router = useRouter()
539
+ return router.useMatch(routeId as any, opts) as any
472
540
  }
473
541
 
474
542
  export function Outlet() {
475
543
  const router = useRouter()
476
- const [, ...matches] = useMatches()
477
-
478
- const childMatch = matches[0]
479
-
480
- if (!childMatch) return null
481
-
482
- const element = ((): React.ReactNode => {
483
- if (!childMatch) {
484
- return null
485
- }
486
-
487
- const errorElement =
488
- childMatch.__.errorElement ?? router.options.defaultErrorElement
489
-
490
- if (childMatch.status === 'error') {
491
- if (errorElement) {
492
- return errorElement as any
493
- }
494
-
495
- if (
496
- childMatch.options.useErrorBoundary ||
497
- router.options.useErrorBoundary
498
- ) {
499
- throw childMatch.error
500
- }
544
+ const matches = useMatches().slice(1)
545
+ const match = matches[0]
501
546
 
502
- return <DefaultErrorBoundary error={childMatch.error} />
503
- }
547
+ const defaultPending = React.useCallback(() => null, [])
504
548
 
505
- if (childMatch.status === 'loading' || childMatch.status === 'idle') {
506
- if (childMatch.isPending) {
507
- const pendingElement =
508
- childMatch.__.pendingElement ?? router.options.defaultPendingElement
549
+ if (!match) {
550
+ return null
551
+ }
509
552
 
510
- if (childMatch.options.pendingMs || pendingElement) {
511
- return (pendingElement as any) ?? null
512
- }
513
- }
553
+ const PendingComponent = (match.__.pendingComponent ??
554
+ router.options.defaultPendingComponent ??
555
+ defaultPending) as any
514
556
 
515
- return null
516
- }
557
+ const errorComponent =
558
+ match.__.errorComponent ?? router.options.defaultErrorComponent
517
559
 
518
- return (childMatch.__.element as any) ?? router.options.defaultElement
519
- })() as JSX.Element
560
+ return (
561
+ <MatchesProvider value={matches}>
562
+ <React.Suspense fallback={<PendingComponent />}>
563
+ <CatchBoundary errorComponent={errorComponent}>
564
+ {
565
+ ((): React.ReactNode => {
566
+ if (match.status === 'error') {
567
+ throw match.error
568
+ }
520
569
 
521
- const catchElement =
522
- childMatch?.options.catchElement ?? router.options.defaultCatchElement
570
+ if (match.status === 'success') {
571
+ return React.createElement(
572
+ (match.__.component as any) ??
573
+ router.options.defaultComponent ??
574
+ Outlet,
575
+ )
576
+ }
523
577
 
524
- return (
525
- <MatchesProvider value={matches} key={childMatch.matchId}>
526
- <CatchBoundary catchElement={catchElement}>{element}</CatchBoundary>
578
+ console.log(match.matchId, 'suspend')
579
+ throw match.__.loadPromise
580
+ })() as JSX.Element
581
+ }
582
+ </CatchBoundary>
583
+ </React.Suspense>
527
584
  </MatchesProvider>
528
585
  )
529
586
  }
530
587
 
531
588
  class CatchBoundary extends React.Component<{
532
589
  children: any
533
- catchElement: any
590
+ errorComponent: any
534
591
  }> {
535
592
  state = {
536
593
  error: false,
@@ -543,19 +600,11 @@ class CatchBoundary extends React.Component<{
543
600
  info,
544
601
  })
545
602
  }
546
- reset = () => {
547
- this.setState({
548
- error: false,
549
- info: false,
550
- })
551
- }
552
603
  render() {
553
- const catchElement = this.props.catchElement ?? DefaultErrorBoundary
604
+ const errorComponent = this.props.errorComponent ?? DefaultErrorBoundary
554
605
 
555
606
  if (this.state.error) {
556
- return typeof catchElement === 'function'
557
- ? catchElement(this.state)
558
- : catchElement
607
+ return React.createElement(errorComponent, this.state)
559
608
  }
560
609
 
561
610
  return this.props.children
@@ -584,19 +633,6 @@ export function DefaultErrorBoundary({ error }: { error: any }) {
584
633
  ) : null}
585
634
  </pre>
586
635
  </div>
587
- <div style={{ height: '1rem' }} />
588
- <div
589
- style={{
590
- fontSize: '.8em',
591
- borderLeft: '3px solid rgba(127, 127, 127, 1)',
592
- paddingLeft: '.5rem',
593
- opacity: 0.5,
594
- }}
595
- >
596
- If you are the owner of this website, it's highly recommended that you
597
- configure your own custom Catch/Error boundaries for the router. You can
598
- optionally configure a boundary for each route.
599
- </div>
600
636
  </div>
601
637
  )
602
638
  }