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

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,25 @@ 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
+ useMatch: <
154
+ TId extends keyof TAllRouteInfo['routeInfoById'],
155
+ TStrict extends boolean = true,
156
+ >(
54
157
  routeId: TId,
55
- ) => RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
158
+ opts?: { strict?: TStrict },
159
+ ) => TStrict extends true
160
+ ? RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
161
+ :
162
+ | RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
163
+ | undefined
56
164
  linkProps: <TTo extends string = '.'>(
57
- props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
58
- React.AnchorHTMLAttributes<HTMLAnchorElement>,
165
+ props: MakeLinkPropsOptions<TAllRouteInfo, '/', TTo>,
59
166
  ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
60
167
  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
- },
168
+ props: MakeLinkOptions<TAllRouteInfo, '/', TTo>,
69
169
  ) => JSX.Element
70
170
  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
- },
171
+ props: MakeMatchRouteOptions<TAllRouteInfo, '/', TTo>,
83
172
  ) => JSX.Element
84
173
  }
85
174
 
@@ -99,53 +188,20 @@ declare module '@tanstack/router-core' {
99
188
  TResolved,
100
189
  ToIdOption<TAllRouteInfo, TRouteInfo['id'], TTo>
101
190
  >,
191
+ opts?: { strict?: boolean },
102
192
  ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TResolved]>
103
193
  linkProps: <TTo extends string = '.'>(
104
- props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
105
- React.AnchorHTMLAttributes<HTMLAnchorElement>,
194
+ props: MakeLinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo>,
106
195
  ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
107
196
  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
- },
197
+ props: MakeLinkOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo>,
116
198
  ) => JSX.Element
117
199
  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
- },
200
+ props: MakeMatchRouteOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo>,
130
201
  ) => JSX.Element
131
202
  }
132
203
  }
133
204
 
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
205
  export type PromptProps = {
150
206
  message: string
151
207
  when?: boolean | any
@@ -154,20 +210,22 @@ export type PromptProps = {
154
210
 
155
211
  //
156
212
 
157
- const matchesContext = React.createContext<RouteMatch[]>(null!)
158
- const routerContext = React.createContext<{ router: Router<any, any> }>(null!)
213
+ export function Link<TTo extends string = '.'>(
214
+ props: MakeLinkOptions<RegisteredAllRouteInfo, '/', TTo>,
215
+ ): JSX.Element {
216
+ const router = useRouter()
217
+ return <router.Link {...(props as any)} />
218
+ }
159
219
 
160
- // Detect if we're in the DOM
161
- const isDOM = Boolean(
162
- typeof window !== 'undefined' &&
163
- window.document &&
164
- window.document.createElement,
165
- )
220
+ type MatchesContextValue = RouteMatch[]
166
221
 
167
- const useLayoutEffect = isDOM ? React.useLayoutEffect : React.useEffect
222
+ export const matchesContext = React.createContext<MatchesContextValue>(null!)
223
+ export const routerContext = React.createContext<{ router: RegisteredRouter }>(
224
+ null!,
225
+ )
168
226
 
169
227
  export type MatchesProviderProps = {
170
- value: RouteMatch[]
228
+ value: MatchesContextValue
171
229
  children: React.ReactNode
172
230
  }
173
231
 
@@ -175,7 +233,7 @@ export function MatchesProvider(props: MatchesProviderProps) {
175
233
  return <matchesContext.Provider {...props} />
176
234
  }
177
235
 
178
- const useRouterSubscription = (router: Router<any, any>) => {
236
+ const useRouterSubscription = (router: Router<any, any, any>) => {
179
237
  useSyncExternalStore(
180
238
  (cb) => router.subscribe(() => cb()),
181
239
  () => router.state,
@@ -185,10 +243,14 @@ const useRouterSubscription = (router: Router<any, any>) => {
185
243
 
186
244
  export function createReactRouter<
187
245
  TRouteConfig extends AnyRouteConfig = RouteConfig,
188
- >(opts: RouterOptions<TRouteConfig>): Router<TRouteConfig> {
246
+ TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
247
+ TRouterContext = unknown,
248
+ >(
249
+ opts: RouterOptions<TRouteConfig, TRouterContext>,
250
+ ): Router<TRouteConfig, TAllRouteInfo, TRouterContext> {
189
251
  const makeRouteExt = (
190
252
  route: AnyRoute,
191
- router: Router<any, any>,
253
+ router: Router<any, any, any>,
192
254
  ): Pick<AnyRoute, 'useRoute' | 'linkProps' | 'Link' | 'MatchRoute'> => {
193
255
  return {
194
256
  useRoute: (subRouteId = '.' as any) => {
@@ -237,7 +299,7 @@ export function createReactRouter<
237
299
  ...rest
238
300
  } = options
239
301
 
240
- const linkInfo = route.buildLink(options)
302
+ const linkInfo = route.buildLink(options as any)
241
303
 
242
304
  if (linkInfo.type === 'external') {
243
305
  const { href } = linkInfo
@@ -253,11 +315,21 @@ export function createReactRouter<
253
315
  next,
254
316
  } = linkInfo
255
317
 
318
+ const reactHandleClick = (e: Event) => {
319
+ if (React.startTransition)
320
+ // This is a hack for react < 18
321
+ React.startTransition(() => {
322
+ handleClick(e)
323
+ })
324
+ else handleClick(e)
325
+ }
326
+
256
327
  const composeHandlers =
257
328
  (handlers: (undefined | ((e: any) => void))[]) =>
258
329
  (e: React.SyntheticEvent) => {
259
- e.persist()
330
+ if (e.persist) e.persist()
260
331
  handlers.forEach((handler) => {
332
+ if (e.defaultPrevented) return
261
333
  if (handler) handler(e)
262
334
  })
263
335
  }
@@ -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
  }
@@ -612,7 +758,7 @@ export function usePrompt(message: string, when: boolean | any): void {
612
758
  unblock()
613
759
  transition.retry()
614
760
  } else {
615
- router.location.pathname = window.location.pathname
761
+ router.state.location.pathname = window.location.pathname
616
762
  }
617
763
  })
618
764