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

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,843 @@ 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 LinkFn<
389
+ TDefaultFrom extends RegisteredRoutesInfo['routePaths'] = '/',
390
+ TDefaultTo extends string = '',
391
+ > {
392
+ <
393
+ TFrom extends RegisteredRoutesInfo['routePaths'] = TDefaultFrom,
394
+ TTo extends string = TDefaultTo,
395
+ >(
396
+ props: MakeLinkOptions<TFrom, TTo> & React.RefAttributes<HTMLAnchorElement>,
397
+ ): ReactNode
193
398
  }
194
399
 
195
- const useRouterSubscription = (router: Router<any, any>) => {
196
- useSyncExternalStore(
197
- (cb) => router.subscribe(() => cb()),
198
- () => router.state,
199
- () => router.state,
400
+ export const Link: LinkFn = React.forwardRef((props: any, ref) => {
401
+ const linkProps = useLinkProps(props)
402
+
403
+ return (
404
+ <a
405
+ {...{
406
+ ref: ref as any,
407
+ ...linkProps,
408
+ children:
409
+ typeof props.children === 'function'
410
+ ? props.children({
411
+ isActive: (linkProps as any)['data-status'] === 'active',
412
+ })
413
+ : props.children,
414
+ }}
415
+ />
200
416
  )
417
+ }) as any
418
+
419
+ export function Navigate<
420
+ TFrom extends RegisteredRoutesInfo['routePaths'] = '/',
421
+ TTo extends string = '',
422
+ >(props: NavigateOptions<RegisteredRoutesInfo, TFrom, TTo>): null {
423
+ const router = useRouter()
424
+
425
+ React.useLayoutEffect(() => {
426
+ router.navigate(props as any)
427
+ }, [])
428
+
429
+ return null
201
430
  }
202
431
 
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
- }
432
+ export const matchIdsContext = React.createContext<string[]>(null!)
433
+ export const routerContext = React.createContext<RegisteredRouter>(null!)
263
434
 
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
- }
435
+ export type RouterProps<
436
+ TRouteConfig extends AnyRoute = AnyRoute,
437
+ TRoutesInfo extends AnyRoutesInfo = DefaultRoutesInfo,
438
+ TDehydrated extends Record<string, any> = Record<string, any>,
439
+ > = Omit<RouterOptions<TRouteConfig, TDehydrated>, 'context'> & {
440
+ router: Router<TRouteConfig, TRoutesInfo>
441
+ context?: Partial<RouterOptions<TRouteConfig, TDehydrated>['context']>
442
+ }
278
443
 
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
- }
444
+ export function useRouterState<TSelected = RegisteredRouter['state']>(opts?: {
445
+ select: (state: RegisteredRouter['state']) => TSelected
446
+ }): TSelected {
447
+ const router = useRouter()
448
+ return useStore(router.__store, opts?.select)
449
+ }
359
450
 
360
- return typeof opts.children === 'function'
361
- ? opts.children(params as any)
362
- : (opts.children as any)
363
- },
364
- }
365
- }
451
+ export function RouterProvider<
452
+ TRouteConfig extends AnyRoute = AnyRoute,
453
+ TRoutesInfo extends AnyRoutesInfo = DefaultRoutesInfo,
454
+ TDehydrated extends Record<string, any> = Record<string, any>,
455
+ >({ router, ...rest }: RouterProps<TRouteConfig, TRoutesInfo, TDehydrated>) {
456
+ router.update(rest)
366
457
 
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
- )
458
+ React.useEffect(() => {
459
+ let unsub
460
+
461
+ React.startTransition(() => {
462
+ unsub = router.mount()
463
+ })
464
+
465
+ return unsub
466
+ }, [router])
467
+
468
+ const Wrap = router.options.Wrap || React.Fragment
469
+
470
+ return (
471
+ <React.Suspense fallback={null}>
472
+ <Wrap>
473
+ <routerContext.Provider value={router as any}>
474
+ <Matches />
475
+ </routerContext.Provider>
476
+ </Wrap>
477
+ </React.Suspense>
478
+ )
479
+ }
382
480
 
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
- },
481
+ function Matches() {
482
+ const router = useRouter()
483
+
484
+ const matchIds = useRouterState({
485
+ select: (state) => {
486
+ const hasPendingComponent = state.pendingMatches.some((d) => {
487
+ const route = router.getRoute(d.routeId as any)
488
+ return !!route?.options.pendingComponent
489
+ })
490
+
491
+ if (hasPendingComponent) {
492
+ console.log('hasPending')
493
+ return state.pendingMatchIds
408
494
  }
409
495
 
410
- const routeExt = makeRouteExt(router.getRoute('/'), router)
496
+ return state.matchIds
497
+ },
498
+ })
499
+
500
+ return (
501
+ <matchIdsContext.Provider value={[undefined!, ...matchIds]}>
502
+ <CatchBoundary
503
+ errorComponent={ErrorComponent}
504
+ onCatch={() => {
505
+ warning(
506
+ false,
507
+ `Error in router! Consider setting an 'errorComponent' in your RootRoute! 👍`,
508
+ )
509
+ }}
510
+ >
511
+ <Outlet />
512
+ </CatchBoundary>
513
+ </matchIdsContext.Provider>
514
+ )
515
+ }
516
+
517
+ export function useRouter(): RegisteredRouter {
518
+ const value = React.useContext(routerContext)
519
+ warning(value, 'useRouter must be used inside a <Router> component!')
520
+ return value
521
+ }
411
522
 
412
- Object.assign(router, routerExt, routeExt)
523
+ export function useMatches<T = RouteMatch[]>(opts?: {
524
+ select?: (matches: RouteMatch[]) => T
525
+ }): T {
526
+ const matchIds = React.useContext(matchIdsContext)
527
+
528
+ return useRouterState({
529
+ select: (state) => {
530
+ const matches = state.matches.slice(
531
+ state.matches.findIndex((d) => d.id === matchIds[0]),
532
+ )
533
+ return (opts?.select?.(matches) ?? matches) as T
413
534
  },
414
- createRoute: ({ router, route }) => {
415
- const routeExt = makeRouteExt(route, router)
535
+ })
536
+ }
537
+
538
+ export function useMatch<
539
+ TFrom extends keyof RegisteredRoutesInfo['routesById'],
540
+ TStrict extends boolean = true,
541
+ TRouteMatchState = RouteMatch<
542
+ RegisteredRoutesInfo,
543
+ RegisteredRoutesInfo['routesById'][TFrom]
544
+ >,
545
+ TSelected = TRouteMatchState,
546
+ >(opts?: {
547
+ from: TFrom
548
+ strict?: TStrict
549
+ select?: (match: TRouteMatchState) => TSelected
550
+ }): TStrict extends true ? TRouteMatchState : TRouteMatchState | undefined {
551
+ const router = useRouter()
552
+ const nearestMatchId = React.useContext(matchIdsContext)[0]!
553
+ const nearestMatchRouteId = router.getRouteMatch(nearestMatchId)?.routeId
554
+
555
+ const matchRouteId = useRouterState({
556
+ select: (state) => {
557
+ const matches = state.matches
558
+ const match = opts?.from
559
+ ? matches.find((d) => d.routeId === opts?.from)
560
+ : matches.find((d) => d.id === nearestMatchId)
416
561
 
417
- Object.assign(route, routeExt)
562
+ return match!.routeId
418
563
  },
419
- loadComponent: async (component) => {
420
- if (component.preload && typeof document !== 'undefined') {
421
- component.preload()
422
- // return await component.preload()
423
- }
564
+ })
565
+
566
+ if (opts?.strict ?? true) {
567
+ invariant(
568
+ nearestMatchRouteId == matchRouteId,
569
+ `useMatch("${
570
+ matchRouteId as string
571
+ }") is being called in a component that is meant to render the '${nearestMatchRouteId}' route. Did you mean to 'useMatch("${
572
+ matchRouteId as string
573
+ }", { strict: false })' or 'useRoute("${
574
+ matchRouteId as string
575
+ }")' instead?`,
576
+ )
577
+ }
424
578
 
425
- return component as any
579
+ const match = useRouterState({
580
+ select: (state) => {
581
+ const matches = state.matches
582
+ const match = opts?.from
583
+ ? matches.find((d) => d.routeId === opts?.from)
584
+ : matches.find((d) => d.id === nearestMatchId)
585
+
586
+ invariant(
587
+ match,
588
+ `Could not find ${
589
+ opts?.from
590
+ ? `an active match from "${opts.from}"`
591
+ : 'a nearest match!'
592
+ }`,
593
+ )
594
+
595
+ return (opts?.select?.(match as any) ?? match) as TSelected
426
596
  },
427
597
  })
428
598
 
429
- return coreRouter as any
599
+ return match as any
430
600
  }
431
601
 
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
602
+ export type RouteFromIdOrRoute<T> = T extends RegisteredRoutesInfo['routeUnion']
603
+ ? T
604
+ : T extends keyof RegisteredRoutesInfo['routesById']
605
+ ? RegisteredRoutesInfo['routesById'][T]
606
+ : T extends string
607
+ ? keyof RegisteredRoutesInfo['routesById']
608
+ : never
609
+
610
+ export function useLoader<
611
+ TFrom extends keyof RegisteredRoutesInfo['routesById'],
612
+ TStrict extends boolean = true,
613
+ TLoader = RegisteredRoutesInfo['routesById'][TFrom]['__types']['loader'],
614
+ TSelected = TLoader,
615
+ >(opts?: {
616
+ from: TFrom
617
+ strict?: TStrict
618
+ select?: (search: TLoader) => TSelected
619
+ }): TStrict extends true ? TSelected : TSelected | undefined {
620
+ return useMatch({
621
+ ...(opts as any),
622
+ select: (match: RouteMatch) =>
623
+ (opts?.select?.(match.loaderData as TLoader) ??
624
+ match.loaderData) as TSelected,
625
+ })
439
626
  }
440
627
 
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)
628
+ export function useRouterContext<
629
+ TFrom extends keyof RegisteredRoutesInfo['routesById'],
630
+ TStrict extends boolean = true,
631
+ TContext = RegisteredRoutesInfo['routesById'][TFrom]['__types']['context'],
632
+ TSelected = TContext,
633
+ >(opts?: {
634
+ from: TFrom
635
+ strict?: TStrict
636
+ select?: (search: TContext) => TSelected
637
+ }): TStrict extends true ? TSelected : TSelected | undefined {
638
+ return useMatch({
639
+ ...(opts as any),
640
+ select: (match: RouteMatch) =>
641
+ (opts?.select?.(match.context as TContext) ?? match.context) as TSelected,
642
+ })
643
+ }
446
644
 
447
- useRouterSubscription(router)
448
- React.useEffect(() => {
449
- return router.mount()
450
- }, [router])
645
+ export function useRouteContext<
646
+ TFrom extends keyof RegisteredRoutesInfo['routesById'],
647
+ TStrict extends boolean = true,
648
+ TRouteContext = RegisteredRoutesInfo['routesById'][TFrom]['__types']['routeContext'],
649
+ TSelected = TRouteContext,
650
+ >(opts?: {
651
+ from: TFrom
652
+ strict?: TStrict
653
+ select?: (search: TRouteContext) => TSelected
654
+ }): TStrict extends true ? TSelected : TSelected | undefined {
655
+ return useMatch({
656
+ ...(opts as any),
657
+ select: (match: RouteMatch) =>
658
+ (opts?.select?.(match.routeContext as TRouteContext) ??
659
+ match.routeContext) as TSelected,
660
+ })
661
+ }
451
662
 
452
- return (
453
- <routerContext.Provider value={{ router }}>
454
- <MatchesProvider value={router.state.matches}>
455
- {children ?? <Outlet />}
456
- </MatchesProvider>
457
- </routerContext.Provider>
663
+ export function useSearch<
664
+ TFrom extends keyof RegisteredRoutesInfo['routesById'],
665
+ TStrict extends boolean = true,
666
+ TSearch = RegisteredRoutesInfo['routesById'][TFrom]['__types']['fullSearchSchema'],
667
+ TSelected = TSearch,
668
+ >(opts?: {
669
+ from: TFrom
670
+ strict?: TStrict
671
+ select?: (search: TSearch) => TSelected
672
+ }): TStrict extends true ? TSelected : TSelected | undefined {
673
+ return useMatch({
674
+ ...(opts as any),
675
+ select: (match: RouteMatch) => {
676
+ return (opts?.select?.(match.search as TSearch) ??
677
+ match.search) as TSelected
678
+ },
679
+ })
680
+ }
681
+
682
+ export function useParams<
683
+ TFrom extends keyof RegisteredRoutesInfo['routesById'] = '/',
684
+ TDefaultSelected = RegisteredRoutesInfo['allParams'] &
685
+ RegisteredRoutesInfo['routesById'][TFrom]['__types']['allParams'],
686
+ TSelected = TDefaultSelected,
687
+ >(opts?: {
688
+ from: TFrom
689
+ select?: (search: TDefaultSelected) => TSelected
690
+ }): TSelected {
691
+ return useRouterState({
692
+ select: (state: any) => {
693
+ const params = (last(state.matches) as any)?.params
694
+ return (opts?.select?.(params) ?? params) as TSelected
695
+ },
696
+ })
697
+ }
698
+
699
+ export function useNavigate<
700
+ TDefaultFrom extends RegisteredRoutesInfo['routePaths'] = '/',
701
+ >(defaultOpts?: { from?: TDefaultFrom }) {
702
+ const router = useRouter()
703
+ return React.useCallback(
704
+ <
705
+ TFrom extends RegisteredRoutesInfo['routePaths'] = TDefaultFrom,
706
+ TTo extends string = '',
707
+ >(
708
+ opts?: NavigateOptions<RegisteredRoutesInfo, TFrom, TTo>,
709
+ ) => {
710
+ return router.navigate({ ...defaultOpts, ...(opts as any) })
711
+ },
712
+ [],
458
713
  )
459
714
  }
460
715
 
461
- export function useRouter(): Router {
462
- const value = React.useContext(routerContext)
463
- warning(!value, 'useRouter must be used inside a <Router> component!')
716
+ export function useMatchRoute() {
717
+ const router = useRouter()
464
718
 
465
- useRouterSubscription(value.router)
719
+ return React.useCallback(
720
+ <TFrom extends string = '/', TTo extends string = ''>(
721
+ opts: MakeUseMatchRouteOptions<TFrom, TTo>,
722
+ ) => {
723
+ const { pending, caseSensitive, ...rest } = opts
466
724
 
467
- return value.router as Router
725
+ return router.matchRoute(rest as any, {
726
+ pending,
727
+ caseSensitive,
728
+ })
729
+ },
730
+ [],
731
+ )
468
732
  }
469
733
 
470
- export function useMatches(): RouteMatch[] {
471
- return React.useContext(matchesContext)
734
+ export function MatchRoute<TFrom extends string = '/', TTo extends string = ''>(
735
+ props: MakeMatchRouteOptions<TFrom, TTo>,
736
+ ): any {
737
+ const matchRoute = useMatchRoute()
738
+ const params = matchRoute(props)
739
+
740
+ if (typeof props.children === 'function') {
741
+ return (props.children as any)(params)
742
+ }
743
+
744
+ return !!params ? props.children : null
472
745
  }
473
746
 
474
747
  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, [])
748
+ const matchIds = React.useContext(matchIdsContext).slice(1)
480
749
 
481
- if (!match) {
750
+ if (!matchIds[0]) {
482
751
  return null
483
752
  }
484
753
 
485
- const PendingComponent = (match.__.pendingComponent ??
754
+ return <Match matchIds={matchIds} />
755
+ }
756
+
757
+ const defaultPending = () => null
758
+
759
+ function Match({ matchIds }: { matchIds: string[] }) {
760
+ const router = useRouter()
761
+ const matchId = matchIds[0]!
762
+ const routeId = router.getRouteMatch(matchId)!.routeId
763
+ const route = router.getRoute(routeId)
764
+
765
+ const PendingComponent = (route.options.pendingComponent ??
486
766
  router.options.defaultPendingComponent ??
487
767
  defaultPending) as any
488
768
 
489
769
  const errorComponent =
490
- match.__.errorComponent ?? router.options.defaultErrorComponent
770
+ route.options.errorComponent ??
771
+ router.options.defaultErrorComponent ??
772
+ ErrorComponent
773
+
774
+ const ResolvedSuspenseBoundary =
775
+ route.options.wrapInSuspense ?? !route.isRoot
776
+ ? React.Suspense
777
+ : SafeFragment
778
+
779
+ const ResolvedCatchBoundary = !!errorComponent ? CatchBoundary : SafeFragment
491
780
 
492
781
  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>
782
+ <matchIdsContext.Provider value={matchIds}>
783
+ <ResolvedSuspenseBoundary
784
+ fallback={React.createElement(PendingComponent, {
785
+ useLoader: route.useLoader,
786
+ useMatch: route.useMatch,
787
+ useContext: route.useContext,
788
+ useRouteContext: route.useRouteContext,
789
+ useSearch: route.useSearch,
790
+ useParams: route.useParams,
791
+ })}
792
+ >
793
+ <ResolvedCatchBoundary
794
+ key={route.id}
795
+ errorComponent={errorComponent}
796
+ onCatch={() => {
797
+ warning(false, `Error in route match: ${matchId}`)
798
+ }}
799
+ >
800
+ <MatchInner matchId={matchId} PendingComponent={PendingComponent} />
801
+ </ResolvedCatchBoundary>
802
+ </ResolvedSuspenseBoundary>
803
+ </matchIdsContext.Provider>
521
804
  )
522
805
  }
523
806
 
807
+ function MatchInner({
808
+ matchId,
809
+ PendingComponent,
810
+ }: {
811
+ matchId: string
812
+ PendingComponent: any
813
+ }): any {
814
+ const router = useRouter()
815
+
816
+ const match = useRouterState({
817
+ select: (d) => {
818
+ const match = d.matchesById[matchId]
819
+ return pick(match!, ['status', 'loadPromise', 'routeId', 'error'])
820
+ },
821
+ })
822
+
823
+ const route = router.getRoute(match.routeId)
824
+
825
+ if (match.status === 'error') {
826
+ throw match.error
827
+ }
828
+
829
+ if (match.status === 'pending') {
830
+ return React.createElement(PendingComponent, {
831
+ useLoader: route.useLoader,
832
+ useMatch: route.useMatch,
833
+ useContext: route.useContext,
834
+ useRouteContext: route.useRouteContext,
835
+ useSearch: route.useSearch,
836
+ useParams: route.useParams,
837
+ })
838
+ }
839
+
840
+ if (match.status === 'success') {
841
+ let comp = route.options.component ?? router.options.defaultComponent
842
+
843
+ if (comp) {
844
+ return React.createElement(comp, {
845
+ useLoader: route.useLoader,
846
+ useMatch: route.useMatch,
847
+ useContext: route.useContext,
848
+ useRouteContext: route.useRouteContext,
849
+ useSearch: route.useSearch,
850
+ useParams: route.useParams,
851
+ })
852
+ }
853
+
854
+ return <Outlet />
855
+ }
856
+
857
+ invariant(
858
+ false,
859
+ 'Idle routeMatch status encountered during rendering! You should never see this. File an issue!',
860
+ )
861
+ }
862
+
863
+ function SafeFragment(props: any) {
864
+ return <>{props.children}</>
865
+ }
866
+
867
+ export function useInjectHtml() {
868
+ const router = useRouter()
869
+
870
+ return React.useCallback(
871
+ (html: string | (() => Promise<string> | string)) => {
872
+ router.injectHtml(html)
873
+ },
874
+ [],
875
+ )
876
+ }
877
+
878
+ export function useDehydrate() {
879
+ const router = useRouter()
880
+
881
+ return React.useCallback(function dehydrate<T>(
882
+ key: any,
883
+ data: T | (() => Promise<T> | T),
884
+ ) {
885
+ return router.dehydrateData(key, data)
886
+ },
887
+ [])
888
+ }
889
+
890
+ export function useHydrate() {
891
+ const router = useRouter()
892
+
893
+ return function hydrate<T = unknown>(key: any) {
894
+ return router.hydrateData(key) as T
895
+ }
896
+ }
897
+
898
+ // This is the messiest thing ever... I'm either seriously tired (likely) or
899
+ // there has to be a better way to reset error boundaries when the
900
+ // router's location key changes.
901
+
524
902
  class CatchBoundary extends React.Component<{
525
903
  children: any
526
904
  errorComponent: any
905
+ onCatch: (error: any, info: any) => void
527
906
  }> {
528
907
  state = {
529
908
  error: false,
909
+ info: undefined,
530
910
  }
531
911
  componentDidCatch(error: any, info: any) {
532
- console.error(error)
533
-
912
+ this.props.onCatch(error, info)
534
913
  this.setState({
535
914
  error,
536
915
  info,
537
916
  })
538
917
  }
539
918
  render() {
540
- const errorComponent = this.props.errorComponent ?? DefaultErrorBoundary
919
+ return (
920
+ <CatchBoundaryInner
921
+ {...this.props}
922
+ errorState={this.state}
923
+ reset={() => this.setState({})}
924
+ />
925
+ )
926
+ }
927
+ }
928
+
929
+ function CatchBoundaryInner(props: {
930
+ children: any
931
+ errorComponent: any
932
+ errorState: { error: unknown; info: any }
933
+ reset: () => void
934
+ }) {
935
+ const locationKey = useRouterState({
936
+ select: (d) => d.resolvedLocation.key,
937
+ })
938
+
939
+ const [activeErrorState, setActiveErrorState] = React.useState(
940
+ props.errorState,
941
+ )
942
+ const errorComponent = props.errorComponent ?? ErrorComponent
943
+ const prevKeyRef = React.useRef('' as any)
541
944
 
542
- if (this.state.error) {
543
- return React.createElement(errorComponent, this.state)
945
+ React.useEffect(() => {
946
+ if (activeErrorState) {
947
+ if (locationKey !== prevKeyRef.current) {
948
+ setActiveErrorState({} as any)
949
+ }
544
950
  }
545
951
 
546
- return this.props.children
952
+ prevKeyRef.current = locationKey
953
+ }, [activeErrorState, locationKey])
954
+
955
+ React.useEffect(() => {
956
+ if (props.errorState.error) {
957
+ setActiveErrorState(props.errorState)
958
+ }
959
+ // props.reset()
960
+ }, [props.errorState.error])
961
+
962
+ if (props.errorState.error && activeErrorState.error) {
963
+ return React.createElement(errorComponent, activeErrorState)
547
964
  }
965
+
966
+ return props.children
548
967
  }
549
968
 
550
- export function DefaultErrorBoundary({ error }: { error: any }) {
969
+ export function ErrorComponent({ error }: { error: any }) {
970
+ const [show, setShow] = React.useState(process.env.NODE_ENV !== 'production')
971
+
551
972
  return (
552
973
  <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>
974
+ <div style={{ display: 'flex', alignItems: 'center', gap: '.5rem' }}>
975
+ <strong style={{ fontSize: '1rem' }}>Something went wrong!</strong>
976
+ <button
977
+ style={{
978
+ appearance: 'none',
979
+ fontSize: '.6em',
980
+ border: '1px solid currentColor',
981
+ padding: '.1rem .2rem',
982
+ fontWeight: 'bold',
983
+ borderRadius: '.25rem',
984
+ }}
985
+ onClick={() => setShow((d) => !d)}
986
+ >
987
+ {show ? 'Hide Error' : 'Show Error'}
988
+ </button>
571
989
  </div>
990
+ <div style={{ height: '.25rem' }} />
991
+ {show ? (
992
+ <div>
993
+ <pre
994
+ style={{
995
+ fontSize: '.7em',
996
+ border: '1px solid red',
997
+ borderRadius: '.25rem',
998
+ padding: '.3rem',
999
+ color: 'red',
1000
+ overflow: 'auto',
1001
+ }}
1002
+ >
1003
+ {error.message ? <code>{error.message}</code> : null}
1004
+ </pre>
1005
+ </div>
1006
+ ) : null}
572
1007
  </div>
573
1008
  )
574
1009
  }
575
1010
 
576
- export function usePrompt(message: string, when: boolean | any): void {
1011
+ export function useBlocker(
1012
+ message: string,
1013
+ condition: boolean | any = true,
1014
+ ): void {
577
1015
  const router = useRouter()
578
1016
 
579
1017
  React.useEffect(() => {
580
- if (!when) return
1018
+ if (!condition) return
581
1019
 
582
- let unblock = router.history.block((transition) => {
1020
+ let unblock = router.history.block((retry, cancel) => {
583
1021
  if (window.confirm(message)) {
584
1022
  unblock()
585
- transition.retry()
586
- } else {
587
- router.location.pathname = window.location.pathname
1023
+ retry()
588
1024
  }
589
1025
  })
590
1026
 
591
1027
  return unblock
592
- }, [when, location, message])
1028
+ })
593
1029
  }
594
1030
 
595
- export function Prompt({ message, when, children }: PromptProps) {
596
- usePrompt(message, when ?? true)
597
- return (children ?? null) as React.ReactNode
1031
+ export function Block({ message, condition, children }: PromptProps) {
1032
+ useBlocker(message, condition)
1033
+ return (children ?? null) as ReactNode
1034
+ }
1035
+
1036
+ export function shallow<T>(objA: T, objB: T) {
1037
+ if (Object.is(objA, objB)) {
1038
+ return true
1039
+ }
1040
+
1041
+ if (
1042
+ typeof objA !== 'object' ||
1043
+ objA === null ||
1044
+ typeof objB !== 'object' ||
1045
+ objB === null
1046
+ ) {
1047
+ return false
1048
+ }
1049
+
1050
+ const keysA = Object.keys(objA)
1051
+ if (keysA.length !== Object.keys(objB).length) {
1052
+ return false
1053
+ }
1054
+
1055
+ for (let i = 0; i < keysA.length; i++) {
1056
+ if (
1057
+ !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
1058
+ !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
1059
+ ) {
1060
+ return false
1061
+ }
1062
+ }
1063
+ return true
598
1064
  }