@tanstack/react-router 0.0.1-beta.15 → 0.0.1-beta.151

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
@@ -1,158 +1,219 @@
1
1
  import * as React from 'react'
2
-
3
- import { useSyncExternalStore } from 'use-sync-external-store/shim'
4
-
2
+ import { NoInfer, useStore } from '@tanstack/react-store'
3
+ import invariant from 'tiny-invariant'
4
+ import warning from 'tiny-warning'
5
5
  import {
6
- AnyRoute,
7
- CheckId,
8
- rootRouteId,
9
- Router,
10
- RouterState,
11
- ToIdOption,
12
- } from '@tanstack/router-core'
13
- import {
14
- warning,
6
+ functionalUpdate,
7
+ last,
8
+ pick,
9
+ RegisteredRoutesInfo,
10
+ MatchRouteOptions,
11
+ RegisteredRouter,
15
12
  RouterOptions,
13
+ Router,
16
14
  RouteMatch,
17
- MatchRouteOptions,
18
- RouteConfig,
19
- AnyRouteConfig,
20
- AnyAllRouteInfo,
21
- DefaultAllRouteInfo,
22
- functionalUpdate,
23
- createRouter,
24
- AnyRouteInfo,
25
- AllRouteInfo,
26
- RouteInfo,
27
- ValidFromPath,
15
+ RouteByPath,
16
+ AnyRoutesInfo,
17
+ DefaultRoutesInfo,
18
+ AnyRoute,
19
+ AnyRouteProps,
28
20
  LinkOptions,
29
- RouteInfoByPath,
30
- ResolveRelativePath,
31
- NoInfer,
32
21
  ToOptions,
33
- invariant,
22
+ ResolveRelativePath,
23
+ NavigateOptions,
24
+ ResolveFullPath,
25
+ ResolveId,
26
+ AnySearchSchema,
27
+ ParsePathParams,
28
+ MergeParamsFromParent,
29
+ RouteContext,
30
+ AnyContext,
31
+ UseLoaderResult,
32
+ ResolveFullSearchSchema,
33
+ Route,
34
34
  } from '@tanstack/router-core'
35
35
 
36
- export * from '@tanstack/router-core'
37
-
38
- export { lazyWithPreload as lazy } from 'react-lazy-with-preload/lib/index'
39
- export type { PreloadableComponent as LazyComponent } from 'react-lazy-with-preload'
36
+ //
40
37
 
41
- type SyncRouteComponent = (props?: {}) => React.ReactNode
42
- export type RouteComponent = SyncRouteComponent & {
43
- preload?: () => Promise<{
44
- default: SyncRouteComponent
45
- }>
46
- }
38
+ export * from '@tanstack/router-core'
39
+ export { useStore }
47
40
 
48
41
  declare module '@tanstack/router-core' {
49
- interface FrameworkGenerics {
50
- Component: RouteComponent
42
+ interface RegisterRouteComponent<TProps extends Record<string, any>> {
43
+ RouteComponent: RouteComponent<TProps>
51
44
  }
52
45
 
53
- interface Router<
54
- TRouteConfig extends AnyRouteConfig = RouteConfig,
55
- TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
56
- > {
57
- useState: () => RouterState
58
- useRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
59
- routeId: TId,
60
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
61
- useMatch: <
62
- TId extends keyof TAllRouteInfo['routeInfoById'],
63
- TStrict extends true | false = true,
64
- >(
65
- routeId: TId,
66
- opts?: { strict?: TStrict },
67
- ) => TStrict extends true
68
- ? RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
69
- :
70
- | RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
71
- | undefined
72
- linkProps: <TTo extends string = '.'>(
73
- props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
74
- React.AnchorHTMLAttributes<HTMLAnchorElement>,
75
- ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
76
- Link: <TTo extends string = '.'>(
77
- props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
78
- React.AnchorHTMLAttributes<HTMLAnchorElement> &
79
- Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
80
- // 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
81
- children?:
82
- | React.ReactNode
83
- | ((state: { isActive: boolean }) => React.ReactNode)
84
- },
85
- ) => JSX.Element
86
- MatchRoute: <TTo extends string = '.'>(
87
- props: ToOptions<TAllRouteInfo, '/', TTo> &
88
- MatchRouteOptions & {
89
- // 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
90
- children?:
91
- | React.ReactNode
92
- | ((
93
- params: RouteInfoByPath<
94
- TAllRouteInfo,
95
- ResolveRelativePath<'/', NoInfer<TTo>>
96
- >['allParams'],
97
- ) => React.ReactNode)
98
- },
99
- ) => JSX.Element
46
+ interface RegisterRouteErrorComponent<TProps extends Record<string, any>> {
47
+ RouteErrorComponent: RouteComponent<TProps>
100
48
  }
101
49
 
50
+ // Extend the Route class to have some React-Specific methods
102
51
  interface Route<
103
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
104
- TRouteInfo extends AnyRouteInfo = RouteInfo,
52
+ TParentRoute extends AnyRoute = AnyRoute,
53
+ TPath extends string = '/',
54
+ TFullPath extends ResolveFullPath<TParentRoute, TPath> = ResolveFullPath<
55
+ TParentRoute,
56
+ TPath
57
+ >,
58
+ TCustomId extends string = string,
59
+ TId extends ResolveId<TParentRoute, TCustomId, TPath> = ResolveId<
60
+ TParentRoute,
61
+ TCustomId,
62
+ TPath
63
+ >,
64
+ TLoader = unknown,
65
+ TSearchSchema extends AnySearchSchema = {},
66
+ TFullSearchSchema extends AnySearchSchema = ResolveFullSearchSchema<
67
+ TParentRoute,
68
+ TSearchSchema
69
+ >,
70
+ TParams extends Record<ParsePathParams<TPath>, any> = Record<
71
+ ParsePathParams<TPath>,
72
+ string
73
+ >,
74
+ TAllParams extends MergeParamsFromParent<
75
+ TParentRoute['__types']['allParams'],
76
+ TParams
77
+ > = MergeParamsFromParent<TParentRoute['__types']['allParams'], TParams>,
78
+ TParentContext extends TParentRoute['__types']['routeContext'] = TParentRoute['__types']['routeContext'],
79
+ TAllParentContext extends TParentRoute['__types']['context'] = TParentRoute['__types']['context'],
80
+ TRouteContext extends RouteContext = RouteContext,
81
+ TContext extends MergeParamsFromParent<
82
+ TParentRoute['__types']['context'],
83
+ TRouteContext
84
+ > = MergeParamsFromParent<
85
+ TParentRoute['__types']['context'],
86
+ TRouteContext
87
+ >,
88
+ TRouterContext extends AnyContext = AnyContext,
89
+ TChildren extends unknown = unknown,
90
+ TRoutesInfo extends DefaultRoutesInfo = DefaultRoutesInfo,
105
91
  > {
106
- useRoute: <
107
- TTo extends string = '.',
108
- TResolved extends string = ResolveRelativePath<
109
- TRouteInfo['id'],
110
- NoInfer<TTo>
111
- >,
112
- >(
113
- routeId: CheckId<
114
- TAllRouteInfo,
115
- TResolved,
116
- ToIdOption<TAllRouteInfo, TRouteInfo['id'], TTo>
117
- >,
118
- opts?: { strict?: boolean },
119
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TResolved]>
120
- linkProps: <TTo extends string = '.'>(
121
- props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
122
- React.AnchorHTMLAttributes<HTMLAnchorElement>,
123
- ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
124
- Link: <TTo extends string = '.'>(
125
- props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
126
- React.AnchorHTMLAttributes<HTMLAnchorElement> &
127
- Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
128
- // 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
129
- children?:
130
- | React.ReactNode
131
- | ((state: { isActive: boolean }) => React.ReactNode)
132
- },
133
- ) => JSX.Element
134
- MatchRoute: <TTo extends string = '.'>(
135
- props: ToOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
136
- MatchRouteOptions & {
137
- // 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
138
- children?:
139
- | React.ReactNode
140
- | ((
141
- params: RouteInfoByPath<
142
- TAllRouteInfo,
143
- ResolveRelativePath<TRouteInfo['fullPath'], NoInfer<TTo>>
144
- >['allParams'],
145
- ) => React.ReactNode)
146
- },
147
- ) => JSX.Element
92
+ useMatch: <TStrict extends boolean = true, TSelected = TContext>(opts?: {
93
+ strict?: TStrict
94
+ select?: (search: TContext) => TSelected
95
+ }) => TStrict extends true ? TSelected : TSelected | undefined
96
+ useLoader: <TStrict extends boolean = true, TSelected = TLoader>(opts?: {
97
+ strict?: TStrict
98
+ select?: (search: TLoader) => TSelected
99
+ }) => TStrict extends true
100
+ ? UseLoaderResult<TSelected>
101
+ : UseLoaderResult<TSelected> | undefined
102
+ useContext: <TStrict extends boolean = true, TSelected = TContext>(opts?: {
103
+ strict?: TStrict
104
+ select?: (search: TContext) => TSelected
105
+ }) => TStrict extends true ? TSelected : TSelected | undefined
106
+ useRouteContext: <
107
+ TStrict extends boolean = true,
108
+ TSelected = TRouteContext,
109
+ >(opts?: {
110
+ strict?: TStrict
111
+ select?: (search: TRouteContext) => TSelected
112
+ }) => TStrict extends true ? TSelected : TSelected | undefined
113
+ useSearch: <
114
+ TStrict extends boolean = true,
115
+ TSelected = TFullSearchSchema,
116
+ >(opts?: {
117
+ strict?: TStrict
118
+ select?: (search: TFullSearchSchema) => TSelected
119
+ }) => TStrict extends true ? TSelected : TSelected | undefined
120
+ useParams: <TStrict extends boolean = true, TSelected = TAllParams>(opts?: {
121
+ strict?: TStrict
122
+ select?: (search: TAllParams) => TSelected
123
+ }) => TStrict extends true ? TSelected : TSelected | undefined
124
+ }
125
+ }
126
+
127
+ Route.__onInit = (route) => {
128
+ Object.assign(route, {
129
+ useMatch: (opts = {}) => {
130
+ return useMatch({ ...opts, from: route.id }) as any
131
+ },
132
+ useLoader: (opts = {}) => {
133
+ return useLoader({ ...opts, from: route.id }) as any
134
+ },
135
+ useContext: (opts: any = {}) => {
136
+ return useMatch({
137
+ ...opts,
138
+ from: route.id,
139
+ select: (d: any) => opts?.select?.(d.context) ?? d.context,
140
+ } as any)
141
+ },
142
+ useRouteContext: (opts: any = {}) => {
143
+ return useMatch({
144
+ ...opts,
145
+ from: route.id,
146
+ select: (d: any) => opts?.select?.(d.routeContext) ?? d.routeContext,
147
+ } as any)
148
+ },
149
+ useSearch: (opts = {}) => {
150
+ return useSearch({ ...opts, from: route.id } as any)
151
+ },
152
+ useParams: (opts = {}) => {
153
+ return useParams({ ...opts, from: route.id } as any)
154
+ },
155
+ })
156
+ }
157
+
158
+ //
159
+
160
+ type ReactNode = any
161
+
162
+ export type SyncRouteComponent<TProps> =
163
+ | ((props: TProps) => ReactNode)
164
+ | React.LazyExoticComponent<(props: TProps) => ReactNode>
165
+
166
+ export type AsyncRouteComponent<TProps> = SyncRouteComponent<TProps> & {
167
+ preload?: () => Promise<void>
168
+ }
169
+
170
+ export type RouteErrorComponent = AsyncRouteComponent<RouteErrorComponentProps>
171
+
172
+ export type RouteErrorComponentProps = {
173
+ error: Error
174
+ info: { componentStack: string }
175
+ }
176
+
177
+ export type AnyRouteComponent = RouteComponent<AnyRouteProps>
178
+
179
+ export type RouteComponent<TProps> = AsyncRouteComponent<TProps>
180
+
181
+ export function lazyRouteComponent<
182
+ T extends Record<string, any>,
183
+ TKey extends keyof T = 'default',
184
+ >(
185
+ importer: () => Promise<T>,
186
+ exportName?: TKey,
187
+ ): T[TKey] extends (props: infer TProps) => any
188
+ ? AsyncRouteComponent<TProps>
189
+ : never {
190
+ let loadPromise: Promise<any>
191
+
192
+ const load = () => {
193
+ if (!loadPromise) {
194
+ loadPromise = importer()
195
+ }
196
+
197
+ return loadPromise
148
198
  }
199
+
200
+ const lazyComp = React.lazy(async () => {
201
+ const moduleExports = await load()
202
+ const comp = moduleExports[exportName ?? 'default']
203
+ return {
204
+ default: comp,
205
+ }
206
+ })
207
+
208
+ ;(lazyComp as any).preload = load
209
+
210
+ return lazyComp as any
149
211
  }
150
212
 
151
- type LinkPropsOptions<
152
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
153
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
154
- TTo extends string = '.',
155
- > = LinkOptions<TAllRouteInfo, TFrom, TTo> & {
213
+ export type LinkPropsOptions<
214
+ TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
215
+ TTo extends string = '',
216
+ > = LinkOptions<RegisteredRoutesInfo, TFrom, TTo> & {
156
217
  // 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)
157
218
  activeProps?:
158
219
  | React.AnchorHTMLAttributes<HTMLAnchorElement>
@@ -161,438 +222,842 @@ type LinkPropsOptions<
161
222
  inactiveProps?:
162
223
  | React.AnchorHTMLAttributes<HTMLAnchorElement>
163
224
  | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)
225
+ // If set to `true`, the link's underlying navigate() call will be wrapped in a `React.startTransition` call. Defaults to `true`.
226
+ startTransition?: boolean
164
227
  }
165
228
 
229
+ export type MakeUseMatchRouteOptions<
230
+ TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
231
+ TTo extends string = '',
232
+ > = ToOptions<RegisteredRoutesInfo, TFrom, TTo> & MatchRouteOptions
233
+
234
+ export type MakeMatchRouteOptions<
235
+ TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
236
+ TTo extends string = '',
237
+ > = ToOptions<RegisteredRoutesInfo, TFrom, TTo> &
238
+ MatchRouteOptions & {
239
+ // 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
240
+ children?:
241
+ | ((
242
+ params?: RouteByPath<
243
+ RegisteredRoutesInfo,
244
+ ResolveRelativePath<TFrom, NoInfer<TTo>>
245
+ >['__types']['allParams'],
246
+ ) => ReactNode)
247
+ | React.ReactNode
248
+ }
249
+
250
+ export type MakeLinkPropsOptions<
251
+ TFrom extends string = '/',
252
+ TTo extends string = '',
253
+ > = LinkPropsOptions<TFrom, TTo> & React.AnchorHTMLAttributes<HTMLAnchorElement>
254
+
255
+ export type MakeLinkOptions<
256
+ TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
257
+ TTo extends string = '',
258
+ > = LinkPropsOptions<TFrom, TTo> &
259
+ Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
260
+ // 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
261
+ children?:
262
+ | React.ReactNode
263
+ | ((state: { isActive: boolean }) => React.ReactNode)
264
+ }
265
+
166
266
  export type PromptProps = {
167
267
  message: string
168
- when?: boolean | any
169
- children?: React.ReactNode
268
+ condition?: boolean | any
269
+ children?: ReactNode
170
270
  }
171
271
 
172
272
  //
173
273
 
174
- const matchesContext = React.createContext<RouteMatch[]>(null!)
175
- const routerContext = React.createContext<{ router: Router<any, any> }>(null!)
274
+ export function useLinkProps<
275
+ TFrom extends string = '/',
276
+ TTo extends string = '',
277
+ >(
278
+ options: MakeLinkPropsOptions<TFrom, TTo>,
279
+ ): React.AnchorHTMLAttributes<HTMLAnchorElement> {
280
+ const router = useRouter()
176
281
 
177
- // Detect if we're in the DOM
178
- const isDOM = Boolean(
179
- typeof window !== 'undefined' &&
180
- window.document &&
181
- window.document.createElement,
182
- )
282
+ const {
283
+ // custom props
284
+ type,
285
+ children,
286
+ target,
287
+ activeProps = () => ({ className: 'active' }),
288
+ inactiveProps = () => ({}),
289
+ activeOptions,
290
+ disabled,
291
+ // fromCurrent,
292
+ hash,
293
+ search,
294
+ params,
295
+ to = '.',
296
+ preload,
297
+ preloadDelay,
298
+ replace,
299
+ // element props
300
+ style,
301
+ className,
302
+ onClick,
303
+ onFocus,
304
+ onMouseEnter,
305
+ onMouseLeave,
306
+ onTouchStart,
307
+ ...rest
308
+ } = options
309
+
310
+ const linkInfo = router.buildLink(options as any)
311
+
312
+ if (linkInfo.type === 'external') {
313
+ const { href } = linkInfo
314
+ return { href }
315
+ }
316
+
317
+ const {
318
+ handleClick,
319
+ handleFocus,
320
+ handleEnter,
321
+ handleLeave,
322
+ handleTouchStart,
323
+ isActive,
324
+ next,
325
+ } = linkInfo
326
+
327
+ const handleReactClick = (e: Event) => {
328
+ if (options.startTransition ?? true) {
329
+ ;(React.startTransition || ((d) => d))(() => {
330
+ handleClick(e)
331
+ })
332
+ }
333
+ }
183
334
 
184
- const useLayoutEffect = isDOM ? React.useLayoutEffect : React.useEffect
335
+ const composeHandlers =
336
+ (handlers: (undefined | ((e: any) => void))[]) =>
337
+ (e: React.SyntheticEvent) => {
338
+ if (e.persist) e.persist()
339
+ handlers.filter(Boolean).forEach((handler) => {
340
+ if (e.defaultPrevented) return
341
+ handler!(e)
342
+ })
343
+ }
185
344
 
186
- export type MatchesProviderProps = {
187
- value: RouteMatch[]
188
- children: React.ReactNode
345
+ // Get the active props
346
+ const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> = isActive
347
+ ? functionalUpdate(activeProps as any, {}) ?? {}
348
+ : {}
349
+
350
+ // Get the inactive props
351
+ const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
352
+ isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
353
+
354
+ return {
355
+ ...resolvedActiveProps,
356
+ ...resolvedInactiveProps,
357
+ ...rest,
358
+ href: disabled ? undefined : next.href,
359
+ onClick: composeHandlers([onClick, handleReactClick]),
360
+ onFocus: composeHandlers([onFocus, handleFocus]),
361
+ onMouseEnter: composeHandlers([onMouseEnter, handleEnter]),
362
+ onMouseLeave: composeHandlers([onMouseLeave, handleLeave]),
363
+ onTouchStart: composeHandlers([onTouchStart, handleTouchStart]),
364
+ target,
365
+ style: {
366
+ ...style,
367
+ ...resolvedActiveProps.style,
368
+ ...resolvedInactiveProps.style,
369
+ },
370
+ className:
371
+ [
372
+ className,
373
+ resolvedActiveProps.className,
374
+ resolvedInactiveProps.className,
375
+ ]
376
+ .filter(Boolean)
377
+ .join(' ') || undefined,
378
+ ...(disabled
379
+ ? {
380
+ role: 'link',
381
+ 'aria-disabled': true,
382
+ }
383
+ : undefined),
384
+ ['data-status']: isActive ? 'active' : undefined,
385
+ }
189
386
  }
190
387
 
191
- export function MatchesProvider(props: MatchesProviderProps) {
192
- return <matchesContext.Provider {...props} />
388
+ export interface LinkComponent<TProps extends Record<string, any> = {}> {
389
+ <
390
+ TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
391
+ TTo extends string = '',
392
+ >(
393
+ props: MakeLinkOptions<TFrom, TTo> &
394
+ TProps &
395
+ React.RefAttributes<HTMLAnchorElement>,
396
+ ): ReactNode
193
397
  }
194
398
 
195
- const useRouterSubscription = (router: Router<any, any>) => {
196
- useSyncExternalStore(
197
- (cb) => router.subscribe(() => cb()),
198
- () => router.state,
199
- () => router.state,
399
+ export const Link: LinkComponent = React.forwardRef((props: any, ref) => {
400
+ const linkProps = useLinkProps(props)
401
+
402
+ return (
403
+ <a
404
+ {...{
405
+ ref: ref as any,
406
+ ...linkProps,
407
+ children:
408
+ typeof props.children === 'function'
409
+ ? props.children({
410
+ isActive: (linkProps as any)['data-status'] === 'active',
411
+ })
412
+ : props.children,
413
+ }}
414
+ />
200
415
  )
416
+ }) as any
417
+
418
+ export function Navigate<
419
+ TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
420
+ TTo extends string = '',
421
+ >(props: NavigateOptions<RegisteredRoutesInfo, TFrom, TTo>): null {
422
+ const router = useRouter()
423
+
424
+ React.useLayoutEffect(() => {
425
+ router.navigate(props as any)
426
+ }, [])
427
+
428
+ return null
201
429
  }
202
430
 
203
- export function createReactRouter<
204
- TRouteConfig extends AnyRouteConfig = RouteConfig,
205
- >(opts: RouterOptions<TRouteConfig>): Router<TRouteConfig> {
206
- const makeRouteExt = (
207
- route: AnyRoute,
208
- router: Router<any, any>,
209
- ): Pick<AnyRoute, 'useRoute' | 'linkProps' | 'Link' | 'MatchRoute'> => {
210
- return {
211
- useRoute: (subRouteId = '.' as any) => {
212
- const resolvedRouteId = router.resolvePath(
213
- route.routeId,
214
- subRouteId as string,
215
- )
216
- const resolvedRoute = router.getRoute(resolvedRouteId)
217
- useRouterSubscription(router)
218
- invariant(
219
- resolvedRoute,
220
- `Could not find a route for route "${
221
- resolvedRouteId as string
222
- }"! Did you forget to add it to your route config?`,
223
- )
224
- return resolvedRoute
225
- },
226
- linkProps: (options) => {
227
- const {
228
- // custom props
229
- type,
230
- children,
231
- target,
232
- activeProps = () => ({ className: 'active' }),
233
- inactiveProps = () => ({}),
234
- activeOptions,
235
- disabled,
236
- // fromCurrent,
237
- hash,
238
- search,
239
- params,
240
- to,
241
- preload,
242
- preloadDelay,
243
- preloadMaxAge,
244
- replace,
245
- // element props
246
- style,
247
- className,
248
- onClick,
249
- onFocus,
250
- onMouseEnter,
251
- onMouseLeave,
252
- onTouchStart,
253
- onTouchEnd,
254
- ...rest
255
- } = options
256
-
257
- const linkInfo = route.buildLink(options)
258
-
259
- if (linkInfo.type === 'external') {
260
- const { href } = linkInfo
261
- return { href }
262
- }
431
+ export const matchIdsContext = React.createContext<string[]>(null!)
432
+ export const routerContext = React.createContext<RegisteredRouter>(null!)
263
433
 
264
- const {
265
- handleClick,
266
- handleFocus,
267
- handleEnter,
268
- handleLeave,
269
- isActive,
270
- next,
271
- } = linkInfo
272
-
273
- const reactHandleClick = (e: Event) => {
274
- React.startTransition(() => {
275
- handleClick(e)
276
- })
277
- }
434
+ export type RouterProps<
435
+ TRouteConfig extends AnyRoute = AnyRoute,
436
+ TRoutesInfo extends AnyRoutesInfo = DefaultRoutesInfo,
437
+ TDehydrated extends Record<string, any> = Record<string, any>,
438
+ > = Omit<RouterOptions<TRouteConfig, TDehydrated>, 'context'> & {
439
+ router: Router<TRouteConfig, TRoutesInfo>
440
+ context?: Partial<RouterOptions<TRouteConfig, TDehydrated>['context']>
441
+ }
278
442
 
279
- const composeHandlers =
280
- (handlers: (undefined | ((e: any) => void))[]) =>
281
- (e: React.SyntheticEvent) => {
282
- e.persist()
283
- handlers.forEach((handler) => {
284
- if (handler) handler(e)
285
- })
286
- }
287
-
288
- // Get the active props
289
- const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> =
290
- isActive ? functionalUpdate(activeProps, {}) ?? {} : {}
291
-
292
- // Get the inactive props
293
- const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
294
- isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
295
-
296
- return {
297
- ...resolvedActiveProps,
298
- ...resolvedInactiveProps,
299
- ...rest,
300
- href: disabled ? undefined : next.href,
301
- onClick: composeHandlers([reactHandleClick, onClick]),
302
- onFocus: composeHandlers([handleFocus, onFocus]),
303
- onMouseEnter: composeHandlers([handleEnter, onMouseEnter]),
304
- onMouseLeave: composeHandlers([handleLeave, onMouseLeave]),
305
- target,
306
- style: {
307
- ...style,
308
- ...resolvedActiveProps.style,
309
- ...resolvedInactiveProps.style,
310
- },
311
- className:
312
- [
313
- className,
314
- resolvedActiveProps.className,
315
- resolvedInactiveProps.className,
316
- ]
317
- .filter(Boolean)
318
- .join(' ') || undefined,
319
- ...(disabled
320
- ? {
321
- role: 'link',
322
- 'aria-disabled': true,
323
- }
324
- : undefined),
325
- ['data-status']: isActive ? 'active' : undefined,
326
- }
327
- },
328
- Link: React.forwardRef((props: any, ref) => {
329
- const linkProps = route.linkProps(props)
330
-
331
- useRouterSubscription(router)
332
-
333
- return (
334
- <a
335
- {...{
336
- ref: ref as any,
337
- ...linkProps,
338
- children:
339
- typeof props.children === 'function'
340
- ? props.children({
341
- isActive: (linkProps as any)['data-status'] === 'active',
342
- })
343
- : props.children,
344
- }}
345
- />
346
- )
347
- }) as any,
348
- MatchRoute: (opts) => {
349
- const { pending, caseSensitive, children, ...rest } = opts
350
-
351
- const params = route.matchRoute(rest as any, {
352
- pending,
353
- caseSensitive,
354
- })
355
-
356
- if (!params) {
357
- return null
358
- }
443
+ export function useRouterState<TSelected = RegisteredRouter['state']>(opts?: {
444
+ select: (state: RegisteredRouter['state']) => TSelected
445
+ }): TSelected {
446
+ const router = useRouter()
447
+ return useStore(router.__store, opts?.select)
448
+ }
359
449
 
360
- return typeof opts.children === 'function'
361
- ? opts.children(params as any)
362
- : (opts.children as any)
363
- },
364
- }
365
- }
450
+ export function RouterProvider<
451
+ TRouteConfig extends AnyRoute = AnyRoute,
452
+ TRoutesInfo extends AnyRoutesInfo = DefaultRoutesInfo,
453
+ TDehydrated extends Record<string, any> = Record<string, any>,
454
+ >({ router, ...rest }: RouterProps<TRouteConfig, TRoutesInfo, TDehydrated>) {
455
+ router.update(rest)
366
456
 
367
- const coreRouter = createRouter<TRouteConfig>({
368
- ...opts,
369
- createRouter: (router) => {
370
- const routerExt: Pick<Router<any, any>, 'useMatch' | 'useState'> = {
371
- useState: () => {
372
- useRouterSubscription(router)
373
- return router.state
374
- },
375
- useMatch: (routeId, opts) => {
376
- useRouterSubscription(router)
377
-
378
- invariant(
379
- routeId !== rootRouteId,
380
- `"${rootRouteId}" cannot be used with useMatch! Did you mean to useRoute("${rootRouteId}")?`,
381
- )
457
+ React.useEffect(() => {
458
+ let unsub
459
+
460
+ React.startTransition(() => {
461
+ unsub = router.mount()
462
+ })
463
+
464
+ return unsub
465
+ }, [router])
466
+
467
+ const Wrap = router.options.Wrap || React.Fragment
468
+
469
+ return (
470
+ <React.Suspense fallback={null}>
471
+ <Wrap>
472
+ <routerContext.Provider value={router as any}>
473
+ <Matches />
474
+ </routerContext.Provider>
475
+ </Wrap>
476
+ </React.Suspense>
477
+ )
478
+ }
382
479
 
383
- const runtimeMatch = useMatches()?.[0]!
384
- const match = router.state.matches.find((d) => d.routeId === routeId)
385
-
386
- if (opts?.strict ?? true) {
387
- invariant(
388
- match,
389
- `Could not find an active match for "${routeId as string}"!`,
390
- )
391
-
392
- invariant(
393
- runtimeMatch.routeId == match?.routeId,
394
- `useMatch('${
395
- match?.routeId as string
396
- }') is being called in a component that is meant to render the '${
397
- runtimeMatch.routeId
398
- }' route. Did you mean to 'useMatch(${
399
- match?.routeId as string
400
- }, { strict: false })' or 'useRoute(${
401
- match?.routeId as string
402
- })' instead?`,
403
- )
404
- }
405
-
406
- return match as any
407
- },
480
+ function Matches() {
481
+ const router = useRouter()
482
+
483
+ const matchIds = useRouterState({
484
+ select: (state) => {
485
+ const hasPendingComponent = state.pendingMatches.some((d) => {
486
+ const route = router.getRoute(d.routeId as any)
487
+ return !!route?.options.pendingComponent
488
+ })
489
+
490
+ if (hasPendingComponent) {
491
+ console.log('hasPending')
492
+ return state.pendingMatchIds
408
493
  }
409
494
 
410
- const routeExt = makeRouteExt(router.getRoute('/'), router)
495
+ return state.matchIds
496
+ },
497
+ })
498
+
499
+ return (
500
+ <matchIdsContext.Provider value={[undefined!, ...matchIds]}>
501
+ <CatchBoundary
502
+ errorComponent={ErrorComponent}
503
+ onCatch={() => {
504
+ warning(
505
+ false,
506
+ `Error in router! Consider setting an 'errorComponent' in your RootRoute! 👍`,
507
+ )
508
+ }}
509
+ >
510
+ <Outlet />
511
+ </CatchBoundary>
512
+ </matchIdsContext.Provider>
513
+ )
514
+ }
515
+
516
+ export function useRouter(): RegisteredRouter {
517
+ const value = React.useContext(routerContext)
518
+ warning(value, 'useRouter must be used inside a <Router> component!')
519
+ return value
520
+ }
411
521
 
412
- Object.assign(router, routerExt, routeExt)
522
+ export function useMatches<T = RouteMatch[]>(opts?: {
523
+ select?: (matches: RouteMatch[]) => T
524
+ }): T {
525
+ const matchIds = React.useContext(matchIdsContext)
526
+
527
+ return useRouterState({
528
+ select: (state) => {
529
+ const matches = state.matches.slice(
530
+ state.matches.findIndex((d) => d.id === matchIds[0]),
531
+ )
532
+ return (opts?.select?.(matches) ?? matches) as T
413
533
  },
414
- createRoute: ({ router, route }) => {
415
- const routeExt = makeRouteExt(route, router)
534
+ })
535
+ }
536
+
537
+ export function useMatch<
538
+ TFrom extends keyof RegisteredRoutesInfo['routesById'],
539
+ TStrict extends boolean = true,
540
+ TRouteMatchState = RouteMatch<
541
+ RegisteredRoutesInfo,
542
+ RegisteredRoutesInfo['routesById'][TFrom]
543
+ >,
544
+ TSelected = TRouteMatchState,
545
+ >(opts?: {
546
+ from: TFrom
547
+ strict?: TStrict
548
+ select?: (match: TRouteMatchState) => TSelected
549
+ }): TStrict extends true ? TRouteMatchState : TRouteMatchState | undefined {
550
+ const router = useRouter()
551
+ const nearestMatchId = React.useContext(matchIdsContext)[0]!
552
+ const nearestMatchRouteId = router.getRouteMatch(nearestMatchId)?.routeId
553
+
554
+ const matchRouteId = useRouterState({
555
+ select: (state) => {
556
+ const matches = state.matches
557
+ const match = opts?.from
558
+ ? matches.find((d) => d.routeId === opts?.from)
559
+ : matches.find((d) => d.id === nearestMatchId)
416
560
 
417
- Object.assign(route, routeExt)
561
+ return match!.routeId
418
562
  },
419
- loadComponent: async (component) => {
420
- if (component.preload && typeof document !== 'undefined') {
421
- component.preload()
422
- // return await component.preload()
423
- }
563
+ })
564
+
565
+ if (opts?.strict ?? true) {
566
+ invariant(
567
+ nearestMatchRouteId == matchRouteId,
568
+ `useMatch("${
569
+ matchRouteId as string
570
+ }") is being called in a component that is meant to render the '${nearestMatchRouteId}' route. Did you mean to 'useMatch("${
571
+ matchRouteId as string
572
+ }", { strict: false })' or 'useRoute("${
573
+ matchRouteId as string
574
+ }")' instead?`,
575
+ )
576
+ }
424
577
 
425
- return component as any
578
+ const match = useRouterState({
579
+ select: (state) => {
580
+ const matches = state.matches
581
+ const match = opts?.from
582
+ ? matches.find((d) => d.routeId === opts?.from)
583
+ : matches.find((d) => d.id === nearestMatchId)
584
+
585
+ invariant(
586
+ match,
587
+ `Could not find ${
588
+ opts?.from
589
+ ? `an active match from "${opts.from}"`
590
+ : 'a nearest match!'
591
+ }`,
592
+ )
593
+
594
+ return (opts?.select?.(match as any) ?? match) as TSelected
426
595
  },
427
596
  })
428
597
 
429
- return coreRouter as any
598
+ return match as any
430
599
  }
431
600
 
432
- export type RouterProps<
433
- TRouteConfig extends AnyRouteConfig = RouteConfig,
434
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
435
- > = RouterOptions<TRouteConfig> & {
436
- router: Router<TRouteConfig, TAllRouteInfo>
437
- // Children will default to `<Outlet />` if not provided
438
- children?: React.ReactNode
601
+ export type RouteFromIdOrRoute<T> = T extends RegisteredRoutesInfo['routeUnion']
602
+ ? T
603
+ : T extends keyof RegisteredRoutesInfo['routesById']
604
+ ? RegisteredRoutesInfo['routesById'][T]
605
+ : T extends string
606
+ ? keyof RegisteredRoutesInfo['routesById']
607
+ : never
608
+
609
+ export function useLoader<
610
+ TFrom extends keyof RegisteredRoutesInfo['routesById'],
611
+ TStrict extends boolean = true,
612
+ TLoader = RegisteredRoutesInfo['routesById'][TFrom]['__types']['loader'],
613
+ TSelected = TLoader,
614
+ >(opts?: {
615
+ from: TFrom
616
+ strict?: TStrict
617
+ select?: (search: TLoader) => TSelected
618
+ }): TStrict extends true ? TSelected : TSelected | undefined {
619
+ return useMatch({
620
+ ...(opts as any),
621
+ select: (match: RouteMatch) =>
622
+ (opts?.select?.(match.loaderData as TLoader) ??
623
+ match.loaderData) as TSelected,
624
+ })
439
625
  }
440
626
 
441
- export function RouterProvider<
442
- TRouteConfig extends AnyRouteConfig = RouteConfig,
443
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
444
- >({ children, router, ...rest }: RouterProps<TRouteConfig, TAllRouteInfo>) {
445
- router.update(rest)
627
+ export function useRouterContext<
628
+ TFrom extends keyof RegisteredRoutesInfo['routesById'],
629
+ TStrict extends boolean = true,
630
+ TContext = RegisteredRoutesInfo['routesById'][TFrom]['__types']['context'],
631
+ TSelected = TContext,
632
+ >(opts?: {
633
+ from: TFrom
634
+ strict?: TStrict
635
+ select?: (search: TContext) => TSelected
636
+ }): TStrict extends true ? TSelected : TSelected | undefined {
637
+ return useMatch({
638
+ ...(opts as any),
639
+ select: (match: RouteMatch) =>
640
+ (opts?.select?.(match.context as TContext) ?? match.context) as TSelected,
641
+ })
642
+ }
446
643
 
447
- useRouterSubscription(router)
448
- React.useEffect(() => {
449
- return router.mount()
450
- }, [router])
644
+ export function useRouteContext<
645
+ TFrom extends keyof RegisteredRoutesInfo['routesById'],
646
+ TStrict extends boolean = true,
647
+ TRouteContext = RegisteredRoutesInfo['routesById'][TFrom]['__types']['routeContext'],
648
+ TSelected = TRouteContext,
649
+ >(opts?: {
650
+ from: TFrom
651
+ strict?: TStrict
652
+ select?: (search: TRouteContext) => TSelected
653
+ }): TStrict extends true ? TSelected : TSelected | undefined {
654
+ return useMatch({
655
+ ...(opts as any),
656
+ select: (match: RouteMatch) =>
657
+ (opts?.select?.(match.routeContext as TRouteContext) ??
658
+ match.routeContext) as TSelected,
659
+ })
660
+ }
451
661
 
452
- return (
453
- <routerContext.Provider value={{ router }}>
454
- <MatchesProvider value={router.state.matches}>
455
- {children ?? <Outlet />}
456
- </MatchesProvider>
457
- </routerContext.Provider>
662
+ export function useSearch<
663
+ TFrom extends keyof RegisteredRoutesInfo['routesById'],
664
+ TStrict extends boolean = true,
665
+ TSearch = RegisteredRoutesInfo['routesById'][TFrom]['__types']['fullSearchSchema'],
666
+ TSelected = TSearch,
667
+ >(opts?: {
668
+ from: TFrom
669
+ strict?: TStrict
670
+ select?: (search: TSearch) => TSelected
671
+ }): TStrict extends true ? TSelected : TSelected | undefined {
672
+ return useMatch({
673
+ ...(opts as any),
674
+ select: (match: RouteMatch) => {
675
+ return (opts?.select?.(match.search as TSearch) ??
676
+ match.search) as TSelected
677
+ },
678
+ })
679
+ }
680
+
681
+ export function useParams<
682
+ TFrom extends keyof RegisteredRoutesInfo['routesById'] = '/',
683
+ TDefaultSelected = RegisteredRoutesInfo['allParams'] &
684
+ RegisteredRoutesInfo['routesById'][TFrom]['__types']['allParams'],
685
+ TSelected = TDefaultSelected,
686
+ >(opts?: {
687
+ from: TFrom
688
+ select?: (search: TDefaultSelected) => TSelected
689
+ }): TSelected {
690
+ return useRouterState({
691
+ select: (state: any) => {
692
+ const params = (last(state.matches) as any)?.params
693
+ return (opts?.select?.(params) ?? params) as TSelected
694
+ },
695
+ })
696
+ }
697
+
698
+ export function useNavigate<
699
+ TDefaultFrom extends RegisteredRoutesInfo['routePaths'] = '/',
700
+ >(defaultOpts?: { from?: TDefaultFrom }) {
701
+ const router = useRouter()
702
+ return React.useCallback(
703
+ <
704
+ TFrom extends RegisteredRoutesInfo['routePaths'] = TDefaultFrom,
705
+ TTo extends string = '',
706
+ >(
707
+ opts?: NavigateOptions<RegisteredRoutesInfo, TFrom, TTo>,
708
+ ) => {
709
+ return router.navigate({ ...defaultOpts, ...(opts as any) })
710
+ },
711
+ [],
458
712
  )
459
713
  }
460
714
 
461
- export function useRouter(): Router {
462
- const value = React.useContext(routerContext)
463
- warning(!value, 'useRouter must be used inside a <Router> component!')
715
+ export function useMatchRoute() {
716
+ const router = useRouter()
464
717
 
465
- useRouterSubscription(value.router)
718
+ return React.useCallback(
719
+ <TFrom extends string = '/', TTo extends string = ''>(
720
+ opts: MakeUseMatchRouteOptions<TFrom, TTo>,
721
+ ) => {
722
+ const { pending, caseSensitive, ...rest } = opts
466
723
 
467
- return value.router as Router
724
+ return router.matchRoute(rest as any, {
725
+ pending,
726
+ caseSensitive,
727
+ })
728
+ },
729
+ [],
730
+ )
468
731
  }
469
732
 
470
- export function useMatches(): RouteMatch[] {
471
- return React.useContext(matchesContext)
733
+ export function MatchRoute<TFrom extends string = '/', TTo extends string = ''>(
734
+ props: MakeMatchRouteOptions<TFrom, TTo>,
735
+ ): any {
736
+ const matchRoute = useMatchRoute()
737
+ const params = matchRoute(props)
738
+
739
+ if (typeof props.children === 'function') {
740
+ return (props.children as any)(params)
741
+ }
742
+
743
+ return !!params ? props.children : null
472
744
  }
473
745
 
474
746
  export function Outlet() {
475
- const router = useRouter()
476
- const matches = useMatches().slice(1)
477
- const match = matches[0]
478
-
479
- const defaultPending = React.useCallback(() => null, [])
747
+ const matchIds = React.useContext(matchIdsContext).slice(1)
480
748
 
481
- if (!match) {
749
+ if (!matchIds[0]) {
482
750
  return null
483
751
  }
484
752
 
485
- const PendingComponent = (match.__.pendingComponent ??
753
+ return <Match matchIds={matchIds} />
754
+ }
755
+
756
+ const defaultPending = () => null
757
+
758
+ function Match({ matchIds }: { matchIds: string[] }) {
759
+ const router = useRouter()
760
+ const matchId = matchIds[0]!
761
+ const routeId = router.getRouteMatch(matchId)!.routeId
762
+ const route = router.getRoute(routeId)
763
+
764
+ const PendingComponent = (route.options.pendingComponent ??
486
765
  router.options.defaultPendingComponent ??
487
766
  defaultPending) as any
488
767
 
489
768
  const errorComponent =
490
- match.__.errorComponent ?? router.options.defaultErrorComponent
769
+ route.options.errorComponent ??
770
+ router.options.defaultErrorComponent ??
771
+ ErrorComponent
772
+
773
+ const ResolvedSuspenseBoundary =
774
+ route.options.wrapInSuspense ?? !route.isRoot
775
+ ? React.Suspense
776
+ : SafeFragment
777
+
778
+ const ResolvedCatchBoundary = !!errorComponent ? CatchBoundary : SafeFragment
491
779
 
492
780
  return (
493
- <MatchesProvider value={matches}>
494
- <React.Suspense fallback={<PendingComponent />}>
495
- <CatchBoundary errorComponent={errorComponent}>
496
- {
497
- ((): React.ReactNode => {
498
- if (match.status === 'error') {
499
- throw match.error
500
- }
501
-
502
- if (match.status === 'success') {
503
- return React.createElement(
504
- (match.__.component as any) ??
505
- router.options.defaultComponent ??
506
- Outlet,
507
- )
508
- }
509
-
510
- if (match.__.loadPromise) {
511
- console.log(match.matchId, 'suspend')
512
- throw match.__.loadPromise
513
- }
514
-
515
- invariant(false, 'This should never happen!')
516
- })() as JSX.Element
517
- }
518
- </CatchBoundary>
519
- </React.Suspense>
520
- </MatchesProvider>
781
+ <matchIdsContext.Provider value={matchIds}>
782
+ <ResolvedSuspenseBoundary
783
+ fallback={React.createElement(PendingComponent, {
784
+ useLoader: route.useLoader,
785
+ useMatch: route.useMatch,
786
+ useContext: route.useContext,
787
+ useRouteContext: route.useRouteContext,
788
+ useSearch: route.useSearch,
789
+ useParams: route.useParams,
790
+ })}
791
+ >
792
+ <ResolvedCatchBoundary
793
+ key={route.id}
794
+ errorComponent={errorComponent}
795
+ onCatch={() => {
796
+ warning(false, `Error in route match: ${matchId}`)
797
+ }}
798
+ >
799
+ <MatchInner matchId={matchId} PendingComponent={PendingComponent} />
800
+ </ResolvedCatchBoundary>
801
+ </ResolvedSuspenseBoundary>
802
+ </matchIdsContext.Provider>
521
803
  )
522
804
  }
523
805
 
806
+ function MatchInner({
807
+ matchId,
808
+ PendingComponent,
809
+ }: {
810
+ matchId: string
811
+ PendingComponent: any
812
+ }): any {
813
+ const router = useRouter()
814
+
815
+ const match = useRouterState({
816
+ select: (d) => {
817
+ const match = d.matchesById[matchId]
818
+ return pick(match!, ['status', 'loadPromise', 'routeId', 'error'])
819
+ },
820
+ })
821
+
822
+ const route = router.getRoute(match.routeId)
823
+
824
+ if (match.status === 'error') {
825
+ throw match.error
826
+ }
827
+
828
+ if (match.status === 'pending') {
829
+ return React.createElement(PendingComponent, {
830
+ useLoader: route.useLoader,
831
+ useMatch: route.useMatch,
832
+ useContext: route.useContext,
833
+ useRouteContext: route.useRouteContext,
834
+ useSearch: route.useSearch,
835
+ useParams: route.useParams,
836
+ })
837
+ }
838
+
839
+ if (match.status === 'success') {
840
+ let comp = route.options.component ?? router.options.defaultComponent
841
+
842
+ if (comp) {
843
+ return React.createElement(comp, {
844
+ useLoader: route.useLoader,
845
+ useMatch: route.useMatch,
846
+ useContext: route.useContext,
847
+ useRouteContext: route.useRouteContext,
848
+ useSearch: route.useSearch,
849
+ useParams: route.useParams,
850
+ })
851
+ }
852
+
853
+ return <Outlet />
854
+ }
855
+
856
+ invariant(
857
+ false,
858
+ 'Idle routeMatch status encountered during rendering! You should never see this. File an issue!',
859
+ )
860
+ }
861
+
862
+ function SafeFragment(props: any) {
863
+ return <>{props.children}</>
864
+ }
865
+
866
+ export function useInjectHtml() {
867
+ const router = useRouter()
868
+
869
+ return React.useCallback(
870
+ (html: string | (() => Promise<string> | string)) => {
871
+ router.injectHtml(html)
872
+ },
873
+ [],
874
+ )
875
+ }
876
+
877
+ export function useDehydrate() {
878
+ const router = useRouter()
879
+
880
+ return React.useCallback(function dehydrate<T>(
881
+ key: any,
882
+ data: T | (() => Promise<T> | T),
883
+ ) {
884
+ return router.dehydrateData(key, data)
885
+ },
886
+ [])
887
+ }
888
+
889
+ export function useHydrate() {
890
+ const router = useRouter()
891
+
892
+ return function hydrate<T = unknown>(key: any) {
893
+ return router.hydrateData(key) as T
894
+ }
895
+ }
896
+
897
+ // This is the messiest thing ever... I'm either seriously tired (likely) or
898
+ // there has to be a better way to reset error boundaries when the
899
+ // router's location key changes.
900
+
524
901
  class CatchBoundary extends React.Component<{
525
902
  children: any
526
903
  errorComponent: any
904
+ onCatch: (error: any, info: any) => void
527
905
  }> {
528
906
  state = {
529
907
  error: false,
908
+ info: undefined,
530
909
  }
531
910
  componentDidCatch(error: any, info: any) {
532
- console.error(error)
533
-
911
+ this.props.onCatch(error, info)
534
912
  this.setState({
535
913
  error,
536
914
  info,
537
915
  })
538
916
  }
539
917
  render() {
540
- const errorComponent = this.props.errorComponent ?? DefaultErrorBoundary
918
+ return (
919
+ <CatchBoundaryInner
920
+ {...this.props}
921
+ errorState={this.state}
922
+ reset={() => this.setState({})}
923
+ />
924
+ )
925
+ }
926
+ }
927
+
928
+ function CatchBoundaryInner(props: {
929
+ children: any
930
+ errorComponent: any
931
+ errorState: { error: unknown; info: any }
932
+ reset: () => void
933
+ }) {
934
+ const locationKey = useRouterState({
935
+ select: (d) => d.resolvedLocation.key,
936
+ })
937
+
938
+ const [activeErrorState, setActiveErrorState] = React.useState(
939
+ props.errorState,
940
+ )
941
+ const errorComponent = props.errorComponent ?? ErrorComponent
942
+ const prevKeyRef = React.useRef('' as any)
541
943
 
542
- if (this.state.error) {
543
- return React.createElement(errorComponent, this.state)
944
+ React.useEffect(() => {
945
+ if (activeErrorState) {
946
+ if (locationKey !== prevKeyRef.current) {
947
+ setActiveErrorState({} as any)
948
+ }
544
949
  }
545
950
 
546
- return this.props.children
951
+ prevKeyRef.current = locationKey
952
+ }, [activeErrorState, locationKey])
953
+
954
+ React.useEffect(() => {
955
+ if (props.errorState.error) {
956
+ setActiveErrorState(props.errorState)
957
+ }
958
+ // props.reset()
959
+ }, [props.errorState.error])
960
+
961
+ if (props.errorState.error && activeErrorState.error) {
962
+ return React.createElement(errorComponent, activeErrorState)
547
963
  }
964
+
965
+ return props.children
548
966
  }
549
967
 
550
- export function DefaultErrorBoundary({ error }: { error: any }) {
968
+ export function ErrorComponent({ error }: { error: any }) {
969
+ const [show, setShow] = React.useState(process.env.NODE_ENV !== 'production')
970
+
551
971
  return (
552
972
  <div style={{ padding: '.5rem', maxWidth: '100%' }}>
553
- <strong style={{ fontSize: '1.2rem' }}>Something went wrong!</strong>
554
- <div style={{ height: '.5rem' }} />
555
- <div>
556
- <pre>
557
- {error.message ? (
558
- <code
559
- style={{
560
- fontSize: '.7em',
561
- border: '1px solid red',
562
- borderRadius: '.25rem',
563
- padding: '.5rem',
564
- color: 'red',
565
- }}
566
- >
567
- {error.message}
568
- </code>
569
- ) : null}
570
- </pre>
973
+ <div style={{ display: 'flex', alignItems: 'center', gap: '.5rem' }}>
974
+ <strong style={{ fontSize: '1rem' }}>Something went wrong!</strong>
975
+ <button
976
+ style={{
977
+ appearance: 'none',
978
+ fontSize: '.6em',
979
+ border: '1px solid currentColor',
980
+ padding: '.1rem .2rem',
981
+ fontWeight: 'bold',
982
+ borderRadius: '.25rem',
983
+ }}
984
+ onClick={() => setShow((d) => !d)}
985
+ >
986
+ {show ? 'Hide Error' : 'Show Error'}
987
+ </button>
571
988
  </div>
989
+ <div style={{ height: '.25rem' }} />
990
+ {show ? (
991
+ <div>
992
+ <pre
993
+ style={{
994
+ fontSize: '.7em',
995
+ border: '1px solid red',
996
+ borderRadius: '.25rem',
997
+ padding: '.3rem',
998
+ color: 'red',
999
+ overflow: 'auto',
1000
+ }}
1001
+ >
1002
+ {error.message ? <code>{error.message}</code> : null}
1003
+ </pre>
1004
+ </div>
1005
+ ) : null}
572
1006
  </div>
573
1007
  )
574
1008
  }
575
1009
 
576
- export function usePrompt(message: string, when: boolean | any): void {
1010
+ export function useBlocker(
1011
+ message: string,
1012
+ condition: boolean | any = true,
1013
+ ): void {
577
1014
  const router = useRouter()
578
1015
 
579
1016
  React.useEffect(() => {
580
- if (!when) return
1017
+ if (!condition) return
581
1018
 
582
- let unblock = router.history.block((transition) => {
1019
+ let unblock = router.history.block((retry, cancel) => {
583
1020
  if (window.confirm(message)) {
584
1021
  unblock()
585
- transition.retry()
586
- } else {
587
- router.location.pathname = window.location.pathname
1022
+ retry()
588
1023
  }
589
1024
  })
590
1025
 
591
1026
  return unblock
592
- }, [when, location, message])
1027
+ })
593
1028
  }
594
1029
 
595
- export function Prompt({ message, when, children }: PromptProps) {
596
- usePrompt(message, when ?? true)
597
- return (children ?? null) as React.ReactNode
1030
+ export function Block({ message, condition, children }: PromptProps) {
1031
+ useBlocker(message, condition)
1032
+ return (children ?? null) as ReactNode
1033
+ }
1034
+
1035
+ export function shallow<T>(objA: T, objB: T) {
1036
+ if (Object.is(objA, objB)) {
1037
+ return true
1038
+ }
1039
+
1040
+ if (
1041
+ typeof objA !== 'object' ||
1042
+ objA === null ||
1043
+ typeof objB !== 'object' ||
1044
+ objB === null
1045
+ ) {
1046
+ return false
1047
+ }
1048
+
1049
+ const keysA = Object.keys(objA)
1050
+ if (keysA.length !== Object.keys(objB).length) {
1051
+ return false
1052
+ }
1053
+
1054
+ for (let i = 0; i < keysA.length; i++) {
1055
+ if (
1056
+ !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
1057
+ !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
1058
+ ) {
1059
+ return false
1060
+ }
1061
+ }
1062
+ return true
598
1063
  }