@tanstack/react-router 0.0.1-beta.3 → 0.0.1-beta.30

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/src/index.tsx CHANGED
@@ -6,7 +6,9 @@ import {
6
6
  AnyRoute,
7
7
  CheckId,
8
8
  rootRouteId,
9
- Router,
9
+ Route,
10
+ RegisteredAllRouteInfo,
11
+ RegisteredRouter,
10
12
  RouterState,
11
13
  ToIdOption,
12
14
  } from '@tanstack/router-core'
@@ -31,15 +33,113 @@ import {
31
33
  NoInfer,
32
34
  ToOptions,
33
35
  invariant,
36
+ Router,
34
37
  } from '@tanstack/router-core'
35
38
 
36
39
  export * from '@tanstack/router-core'
37
40
 
41
+ export type SyncRouteComponent<TProps = {}> = (
42
+ props: TProps,
43
+ ) => JSX.Element | React.ReactNode
44
+
45
+ export type RouteComponent<TProps = {}> = SyncRouteComponent<TProps> & {
46
+ preload?: () => Promise<SyncRouteComponent<TProps>>
47
+ }
48
+
49
+ export function lazy(
50
+ importer: () => Promise<{ default: SyncRouteComponent }>,
51
+ ): RouteComponent {
52
+ const lazyComp = React.lazy(importer as any)
53
+ let promise: Promise<SyncRouteComponent>
54
+ let resolvedComp: SyncRouteComponent
55
+
56
+ const forwardedComp = React.forwardRef((props, ref) => {
57
+ const resolvedCompRef = React.useRef(resolvedComp || lazyComp)
58
+ return React.createElement(
59
+ resolvedCompRef.current as any,
60
+ { ...(ref ? { ref } : {}), ...props } as any,
61
+ )
62
+ })
63
+
64
+ const finalComp = forwardedComp as unknown as RouteComponent
65
+
66
+ finalComp.preload = () => {
67
+ if (!promise) {
68
+ promise = importer().then((module) => {
69
+ resolvedComp = module.default
70
+ return resolvedComp
71
+ })
72
+ }
73
+
74
+ return promise
75
+ }
76
+
77
+ return finalComp
78
+ }
79
+
80
+ type LinkPropsOptions<
81
+ TAllRouteInfo extends AnyAllRouteInfo,
82
+ TFrom extends ValidFromPath<TAllRouteInfo>,
83
+ TTo extends string,
84
+ > = LinkOptions<TAllRouteInfo, TFrom, TTo> & {
85
+ // A function that returns additional props for the `active` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
86
+ activeProps?:
87
+ | React.AnchorHTMLAttributes<HTMLAnchorElement>
88
+ | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)
89
+ // A function that returns additional props for the `inactive` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
90
+ inactiveProps?:
91
+ | React.AnchorHTMLAttributes<HTMLAnchorElement>
92
+ | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)
93
+ }
94
+
95
+ type MakeMatchRouteOptions<
96
+ TAllRouteInfo extends AnyAllRouteInfo,
97
+ TFrom extends ValidFromPath<TAllRouteInfo>,
98
+ TTo extends string,
99
+ > = ToOptions<TAllRouteInfo, TFrom, TTo> &
100
+ MatchRouteOptions & {
101
+ // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
102
+ children?:
103
+ | React.ReactNode
104
+ | ((
105
+ params: RouteInfoByPath<
106
+ TAllRouteInfo,
107
+ ResolveRelativePath<TFrom, NoInfer<TTo>>
108
+ >['allParams'],
109
+ ) => React.ReactNode)
110
+ }
111
+
112
+ type MakeLinkPropsOptions<
113
+ TAllRouteInfo extends AnyAllRouteInfo,
114
+ TFrom extends ValidFromPath<TAllRouteInfo>,
115
+ TTo extends string,
116
+ > = LinkPropsOptions<TAllRouteInfo, TFrom, TTo> &
117
+ React.AnchorHTMLAttributes<HTMLAnchorElement>
118
+
119
+ type MakeLinkOptions<
120
+ TAllRouteInfo extends AnyAllRouteInfo,
121
+ TFrom extends ValidFromPath<TAllRouteInfo>,
122
+ TTo extends string,
123
+ > = LinkPropsOptions<TAllRouteInfo, TFrom, TTo> &
124
+ React.AnchorHTMLAttributes<HTMLAnchorElement> &
125
+ Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
126
+ // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
127
+ children?:
128
+ | React.ReactNode
129
+ | ((state: { isActive: boolean }) => React.ReactNode)
130
+ }
131
+
38
132
  declare module '@tanstack/router-core' {
39
133
  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>)
134
+ Component: RouteComponent
135
+ ErrorComponent: RouteComponent<{
136
+ error: unknown
137
+ info: { componentStack: string }
138
+ }>
139
+ }
140
+
141
+ interface RouterOptions<TRouteConfig, TRouterContext> {
142
+ // ssrFooter?: () => JSX.Element | React.ReactNode
43
143
  }
44
144
 
45
145
  interface Router<
@@ -50,36 +150,26 @@ declare module '@tanstack/router-core' {
50
150
  useRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
51
151
  routeId: TId,
52
152
  ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
53
- useMatch: <TId extends keyof TAllRouteInfo['routeInfoById']>(
153
+ useNearestMatch: () => RouteMatch<TAllRouteInfo, RouteInfo>
154
+ useMatch: <
155
+ TId extends keyof TAllRouteInfo['routeInfoById'],
156
+ TStrict extends boolean = true,
157
+ >(
54
158
  routeId: TId,
55
- ) => RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
159
+ opts?: { strict?: TStrict },
160
+ ) => TStrict extends true
161
+ ? RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
162
+ :
163
+ | RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
164
+ | undefined
56
165
  linkProps: <TTo extends string = '.'>(
57
- props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
58
- React.AnchorHTMLAttributes<HTMLAnchorElement>,
166
+ props: MakeLinkPropsOptions<TAllRouteInfo, '/', TTo>,
59
167
  ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
60
168
  Link: <TTo extends string = '.'>(
61
- props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
62
- React.AnchorHTMLAttributes<HTMLAnchorElement> &
63
- Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
64
- // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
65
- children?:
66
- | React.ReactNode
67
- | ((state: { isActive: boolean }) => React.ReactNode)
68
- },
169
+ props: MakeLinkOptions<TAllRouteInfo, '/', TTo>,
69
170
  ) => JSX.Element
70
171
  MatchRoute: <TTo extends string = '.'>(
71
- props: ToOptions<TAllRouteInfo, '/', TTo> &
72
- MatchRouteOptions & {
73
- // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
74
- children?:
75
- | React.ReactNode
76
- | ((
77
- params: RouteInfoByPath<
78
- TAllRouteInfo,
79
- ResolveRelativePath<'/', NoInfer<TTo>>
80
- >['allParams'],
81
- ) => React.ReactNode)
82
- },
172
+ props: MakeMatchRouteOptions<TAllRouteInfo, '/', TTo>,
83
173
  ) => JSX.Element
84
174
  }
85
175
 
@@ -99,53 +189,20 @@ declare module '@tanstack/router-core' {
99
189
  TResolved,
100
190
  ToIdOption<TAllRouteInfo, TRouteInfo['id'], TTo>
101
191
  >,
192
+ opts?: { strict?: boolean },
102
193
  ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TResolved]>
103
194
  linkProps: <TTo extends string = '.'>(
104
- props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
105
- React.AnchorHTMLAttributes<HTMLAnchorElement>,
195
+ props: MakeLinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo>,
106
196
  ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
107
197
  Link: <TTo extends string = '.'>(
108
- props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
109
- React.AnchorHTMLAttributes<HTMLAnchorElement> &
110
- Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
111
- // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
112
- children?:
113
- | React.ReactNode
114
- | ((state: { isActive: boolean }) => React.ReactNode)
115
- },
198
+ props: MakeLinkOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo>,
116
199
  ) => JSX.Element
117
200
  MatchRoute: <TTo extends string = '.'>(
118
- props: ToOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
119
- MatchRouteOptions & {
120
- // If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
121
- children?:
122
- | React.ReactNode
123
- | ((
124
- params: RouteInfoByPath<
125
- TAllRouteInfo,
126
- ResolveRelativePath<TRouteInfo['fullPath'], NoInfer<TTo>>
127
- >['allParams'],
128
- ) => React.ReactNode)
129
- },
201
+ props: MakeMatchRouteOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo>,
130
202
  ) => JSX.Element
131
203
  }
132
204
  }
133
205
 
134
- type LinkPropsOptions<
135
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
136
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
137
- TTo extends string = '.',
138
- > = LinkOptions<TAllRouteInfo, TFrom, TTo> & {
139
- // A function that returns additional props for the `active` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
140
- activeProps?:
141
- | React.AnchorHTMLAttributes<HTMLAnchorElement>
142
- | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)
143
- // A function that returns additional props for the `inactive` state of this link. These props override other props passed to the link (`style`'s are merged, `className`'s are concatenated)
144
- inactiveProps?:
145
- | React.AnchorHTMLAttributes<HTMLAnchorElement>
146
- | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)
147
- }
148
-
149
206
  export type PromptProps = {
150
207
  message: string
151
208
  when?: boolean | any
@@ -154,20 +211,22 @@ export type PromptProps = {
154
211
 
155
212
  //
156
213
 
157
- const matchesContext = React.createContext<RouteMatch[]>(null!)
158
- const routerContext = React.createContext<{ router: Router<any, any> }>(null!)
214
+ export function Link<TTo extends string = '.'>(
215
+ props: MakeLinkOptions<RegisteredAllRouteInfo, '/', TTo>,
216
+ ): JSX.Element {
217
+ const router = useRouter()
218
+ return <router.Link {...(props as any)} />
219
+ }
159
220
 
160
- // Detect if we're in the DOM
161
- const isDOM = Boolean(
162
- typeof window !== 'undefined' &&
163
- window.document &&
164
- window.document.createElement,
165
- )
221
+ type MatchesContextValue = RouteMatch[]
166
222
 
167
- const useLayoutEffect = isDOM ? React.useLayoutEffect : React.useEffect
223
+ export const matchesContext = React.createContext<MatchesContextValue>(null!)
224
+ export const routerContext = React.createContext<{ router: RegisteredRouter }>(
225
+ null!,
226
+ )
168
227
 
169
228
  export type MatchesProviderProps = {
170
- value: RouteMatch[]
229
+ value: MatchesContextValue
171
230
  children: React.ReactNode
172
231
  }
173
232
 
@@ -175,7 +234,7 @@ export function MatchesProvider(props: MatchesProviderProps) {
175
234
  return <matchesContext.Provider {...props} />
176
235
  }
177
236
 
178
- const useRouterSubscription = (router: Router<any, any>) => {
237
+ const useRouterSubscription = (router: Router<any, any, any>) => {
179
238
  useSyncExternalStore(
180
239
  (cb) => router.subscribe(() => cb()),
181
240
  () => router.state,
@@ -185,10 +244,14 @@ const useRouterSubscription = (router: Router<any, any>) => {
185
244
 
186
245
  export function createReactRouter<
187
246
  TRouteConfig extends AnyRouteConfig = RouteConfig,
188
- >(opts: RouterOptions<TRouteConfig>): Router<TRouteConfig> {
247
+ TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
248
+ TRouterContext = unknown,
249
+ >(
250
+ opts: RouterOptions<TRouteConfig, TRouterContext>,
251
+ ): Router<TRouteConfig, TAllRouteInfo, TRouterContext> {
189
252
  const makeRouteExt = (
190
253
  route: AnyRoute,
191
- router: Router<any, any>,
254
+ router: Router<any, any, any>,
192
255
  ): Pick<AnyRoute, 'useRoute' | 'linkProps' | 'Link' | 'MatchRoute'> => {
193
256
  return {
194
257
  useRoute: (subRouteId = '.' as any) => {
@@ -237,7 +300,7 @@ export function createReactRouter<
237
300
  ...rest
238
301
  } = options
239
302
 
240
- const linkInfo = route.buildLink(options)
303
+ const linkInfo = route.buildLink(options as any)
241
304
 
242
305
  if (linkInfo.type === 'external') {
243
306
  const { href } = linkInfo
@@ -253,6 +316,15 @@ export function createReactRouter<
253
316
  next,
254
317
  } = linkInfo
255
318
 
319
+ const reactHandleClick = (e: Event) => {
320
+ if (React.startTransition)
321
+ // This is a hack for react < 18
322
+ React.startTransition(() => {
323
+ handleClick(e)
324
+ })
325
+ else handleClick(e)
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]),
@@ -344,12 +416,12 @@ export function createReactRouter<
344
416
  const coreRouter = createRouter<TRouteConfig>({
345
417
  ...opts,
346
418
  createRouter: (router) => {
347
- const routerExt: Pick<Router<any, any>, 'useMatch' | 'useState'> = {
419
+ const routerExt: Pick<Router<any, any, any>, 'useMatch' | 'useState'> = {
348
420
  useState: () => {
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,36 +429,34 @@ 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 nearestMatch = useNearestMatch()
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
+ nearestMatch.routeId == match?.routeId,
443
+ `useMatch("${
444
+ match?.routeId as string
445
+ }") is being called in a component that is meant to render the '${
446
+ nearestMatch.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
 
389
- const routeExt = makeRouteExt(router.getRoute('/'), router)
459
+ const routeExt = makeRouteExt(router.getRoute(rootRouteId), router)
390
460
 
391
461
  Object.assign(router, routerExt, routeExt)
392
462
  },
@@ -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
 
@@ -417,125 +481,184 @@ export function createReactRouter<
417
481
  export type RouterProps<
418
482
  TRouteConfig extends AnyRouteConfig = RouteConfig,
419
483
  TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
420
- > = RouterOptions<TRouteConfig> & {
421
- router: Router<TRouteConfig, TAllRouteInfo>
422
- // Children will default to `<Outlet />` if not provided
423
- children?: React.ReactNode
484
+ TRouterContext = unknown,
485
+ > = RouterOptions<TRouteConfig, TRouterContext> & {
486
+ router: Router<TRouteConfig, TAllRouteInfo, TRouterContext>
424
487
  }
425
488
 
426
489
  export function RouterProvider<
427
490
  TRouteConfig extends AnyRouteConfig = RouteConfig,
428
491
  TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
429
- >({ children, router, ...rest }: RouterProps<TRouteConfig, TAllRouteInfo>) {
492
+ TRouterContext = unknown,
493
+ >({
494
+ router,
495
+ ...rest
496
+ }: RouterProps<TRouteConfig, TAllRouteInfo, TRouterContext>) {
430
497
  router.update(rest)
431
498
 
432
499
  useRouterSubscription(router)
433
-
434
- useLayoutEffect(() => {
500
+ React.useEffect(() => {
435
501
  return router.mount()
436
502
  }, [router])
437
503
 
438
504
  return (
439
- <routerContext.Provider value={{ router }}>
440
- <MatchesProvider value={router.state.matches}>
441
- {children ?? <Outlet />}
442
- </MatchesProvider>
443
- </routerContext.Provider>
505
+ <>
506
+ <routerContext.Provider value={{ router: router as any }}>
507
+ <MatchesProvider value={[undefined!, ...router.state.matches]}>
508
+ <Outlet />
509
+ </MatchesProvider>
510
+ </routerContext.Provider>
511
+ </>
444
512
  )
445
513
  }
446
514
 
447
- function useRouter(): Router {
515
+ export function useRouter(): RegisteredRouter {
448
516
  const value = React.useContext(routerContext)
449
517
  warning(!value, 'useRouter must be used inside a <Router> component!')
450
518
 
451
519
  useRouterSubscription(value.router)
452
520
 
453
- return value.router as Router
521
+ return value.router
454
522
  }
455
523
 
456
- function useMatches(): RouteMatch[] {
524
+ export function useMatches(): RouteMatch[] {
457
525
  return React.useContext(matchesContext)
458
526
  }
459
527
 
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
472
- }
473
-
474
- export function Outlet() {
528
+ export function useMatch<
529
+ TId extends keyof RegisteredAllRouteInfo['routeInfoById'],
530
+ TStrict extends boolean = true,
531
+ >(
532
+ routeId: TId,
533
+ opts?: { strict?: TStrict },
534
+ ): TStrict extends true
535
+ ? RouteMatch<
536
+ RegisteredAllRouteInfo,
537
+ RegisteredAllRouteInfo['routeInfoById'][TId]
538
+ >
539
+ :
540
+ | RouteMatch<
541
+ RegisteredAllRouteInfo,
542
+ RegisteredAllRouteInfo['routeInfoById'][TId]
543
+ >
544
+ | undefined {
475
545
  const router = useRouter()
476
- const [, ...matches] = useMatches()
546
+ return router.useMatch(routeId as any, opts) as any
547
+ }
477
548
 
478
- const childMatch = matches[0]
549
+ export function useNearestMatch(): RouteMatch<
550
+ RegisteredAllRouteInfo,
551
+ RouteInfo
552
+ > {
553
+ const runtimeMatch = useMatches()[0]
479
554
 
480
- if (!childMatch) return null
555
+ invariant(runtimeMatch, `Could not find a nearest match!`)
481
556
 
482
- const element = ((): React.ReactNode => {
483
- if (!childMatch) {
484
- return null
485
- }
557
+ return runtimeMatch as any
558
+ }
486
559
 
487
- const errorElement =
488
- childMatch.__.errorElement ?? router.options.defaultErrorElement
560
+ export function useRoute<
561
+ TId extends keyof RegisteredAllRouteInfo['routeInfoById'],
562
+ >(
563
+ routeId: TId,
564
+ ): Route<RegisteredAllRouteInfo, RegisteredAllRouteInfo['routeInfoById'][TId]> {
565
+ const router = useRouter()
566
+ return router.useRoute(routeId as any) as any
567
+ }
489
568
 
490
- if (childMatch.status === 'error') {
491
- if (errorElement) {
492
- return errorElement as any
493
- }
569
+ export function useSearch<
570
+ TId extends keyof RegisteredAllRouteInfo['routeInfoById'] = keyof RegisteredAllRouteInfo['routeInfoById'],
571
+ >(_routeId?: TId): RegisteredAllRouteInfo['fullSearchSchema'] {
572
+ return useRouter().state.location.search
573
+ }
494
574
 
495
- if (
496
- childMatch.options.useErrorBoundary ||
497
- router.options.useErrorBoundary
498
- ) {
499
- throw childMatch.error
500
- }
575
+ export function linkProps<TTo extends string = '.'>(
576
+ props: MakeLinkPropsOptions<RegisteredAllRouteInfo, '/', TTo>,
577
+ ): React.AnchorHTMLAttributes<HTMLAnchorElement> {
578
+ const router = useRouter()
579
+ return router.linkProps(props as any)
580
+ }
501
581
 
502
- return <DefaultErrorBoundary error={childMatch.error} />
503
- }
582
+ export function MatchRoute<TTo extends string = '.'>(
583
+ props: MakeMatchRouteOptions<RegisteredAllRouteInfo, '/', TTo>,
584
+ ): JSX.Element {
585
+ const router = useRouter()
586
+ return React.createElement(router.MatchRoute, props as any)
587
+ }
504
588
 
505
- if (childMatch.status === 'loading' || childMatch.status === 'idle') {
506
- if (childMatch.isPending) {
507
- const pendingElement =
508
- childMatch.__.pendingElement ?? router.options.defaultPendingElement
589
+ export function Outlet() {
590
+ const router = useRouter()
591
+ const matches = useMatches().slice(1)
592
+ const match = matches[0]
509
593
 
510
- if (childMatch.options.pendingMs || pendingElement) {
511
- return (pendingElement as any) ?? null
512
- }
513
- }
594
+ const defaultPending = React.useCallback(() => null, [])
514
595
 
515
- return null
516
- }
596
+ if (!match) {
597
+ return null
598
+ }
517
599
 
518
- return (childMatch.__.element as any) ?? router.options.defaultElement
519
- })() as JSX.Element
600
+ const PendingComponent = (match.__.pendingComponent ??
601
+ router.options.defaultPendingComponent ??
602
+ defaultPending) as any
520
603
 
521
- const catchElement =
522
- childMatch?.options.catchElement ?? router.options.defaultCatchElement
604
+ const errorComponent =
605
+ match.__.errorComponent ?? router.options.defaultErrorComponent
523
606
 
524
607
  return (
525
- <MatchesProvider value={matches} key={childMatch.matchId}>
526
- <CatchBoundary catchElement={catchElement}>{element}</CatchBoundary>
608
+ <MatchesProvider value={matches}>
609
+ <React.Suspense fallback={<PendingComponent />}>
610
+ <CatchBoundary
611
+ key={match.routeId}
612
+ errorComponent={errorComponent}
613
+ match={match as any}
614
+ >
615
+ {
616
+ ((): React.ReactNode => {
617
+ if (match.status === 'error') {
618
+ throw match.error
619
+ }
620
+
621
+ if (match.status === 'success') {
622
+ return React.createElement(
623
+ (match.__.component as any) ??
624
+ router.options.defaultComponent ??
625
+ Outlet,
626
+ )
627
+ }
628
+ throw match.__.loadPromise
629
+ })() as JSX.Element
630
+ }
631
+ </CatchBoundary>
632
+ </React.Suspense>
633
+ {/* Provide a suffix suspense boundary to make sure the router is
634
+ ready to be dehydrated on the server */}
635
+ {/* {router.options.ssrFooter && match.matchId === rootRouteId ? (
636
+ <React.Suspense fallback={null}>
637
+ {(() => {
638
+ if (router.state.pending) {
639
+ throw router.navigationPromise
640
+ }
641
+
642
+ return router.options.ssrFooter()
643
+ })()}
644
+ </React.Suspense>
645
+ ) : null} */}
527
646
  </MatchesProvider>
528
647
  )
529
648
  }
530
649
 
531
650
  class CatchBoundary extends React.Component<{
532
651
  children: any
533
- catchElement: any
652
+ errorComponent: any
653
+ match: RouteMatch
534
654
  }> {
535
655
  state = {
536
656
  error: false,
657
+ info: undefined,
537
658
  }
659
+
538
660
  componentDidCatch(error: any, info: any) {
661
+ console.error(`Error in route match: ${this.props.match.matchId}`)
539
662
  console.error(error)
540
663
 
541
664
  this.setState({
@@ -543,23 +666,59 @@ class CatchBoundary extends React.Component<{
543
666
  info,
544
667
  })
545
668
  }
546
- reset = () => {
547
- this.setState({
548
- error: false,
549
- info: false,
550
- })
551
- }
669
+
552
670
  render() {
553
- const catchElement = this.props.catchElement ?? DefaultErrorBoundary
671
+ return (
672
+ <CatchBoundaryInner
673
+ {...this.props}
674
+ errorState={this.state}
675
+ reset={() => this.setState({})}
676
+ />
677
+ )
678
+ }
679
+ }
680
+
681
+ // This is the messiest thing ever... I'm either seriously tired (likely) or
682
+ // there has to be a better way to reset error boundaries when the
683
+ // router's location key changes.
684
+ function CatchBoundaryInner(props: {
685
+ children: any
686
+ errorComponent: any
687
+ errorState: { error: unknown; info: any }
688
+ reset: () => void
689
+ }) {
690
+ const [activeErrorState, setActiveErrorState] = React.useState(
691
+ props.errorState,
692
+ )
693
+ const router = useRouter()
694
+ const errorComponent = props.errorComponent ?? DefaultErrorBoundary
695
+
696
+ React.useEffect(() => {
697
+ if (activeErrorState) {
698
+ let prevKey = router.state.location.key
699
+ return router.subscribe(() => {
700
+ if (router.state.location.key !== prevKey) {
701
+ prevKey = router.state.location.key
702
+ setActiveErrorState({} as any)
703
+ }
704
+ })
705
+ }
706
+
707
+ return
708
+ }, [activeErrorState])
554
709
 
555
- if (this.state.error) {
556
- return typeof catchElement === 'function'
557
- ? catchElement(this.state)
558
- : catchElement
710
+ React.useEffect(() => {
711
+ if (props.errorState.error) {
712
+ setActiveErrorState(props.errorState)
559
713
  }
714
+ props.reset()
715
+ }, [props.errorState.error])
560
716
 
561
- return this.props.children
717
+ if (activeErrorState.error) {
718
+ return React.createElement(errorComponent, activeErrorState)
562
719
  }
720
+
721
+ return props.children
563
722
  }
564
723
 
565
724
  export function DefaultErrorBoundary({ error }: { error: any }) {
@@ -584,19 +743,6 @@ export function DefaultErrorBoundary({ error }: { error: any }) {
584
743
  ) : null}
585
744
  </pre>
586
745
  </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
746
  </div>
601
747
  )
602
748
  }