@tanstack/react-router 0.0.1-beta.14 → 0.0.1-beta.145

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