@tanstack/react-router 0.0.1-beta.13 → 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,440 +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()
382
481
 
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
- },
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
+ })
488
+
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
552
+
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)
416
559
 
417
- Object.assign(route, routeExt)
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
- useLayoutEffect(() => {
449
- const unsub = router.mount()
450
- router.load()
451
- return unsub
452
- }, [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
+ }
453
660
 
454
- return (
455
- <routerContext.Provider value={{ router }}>
456
- <MatchesProvider value={router.state.matches}>
457
- {children ?? <Outlet />}
458
- </MatchesProvider>
459
- </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
+ [],
460
711
  )
461
712
  }
462
713
 
463
- export function useRouter(): Router {
464
- const value = React.useContext(routerContext)
465
- warning(!value, 'useRouter must be used inside a <Router> component!')
714
+ export function useMatchRoute() {
715
+ const router = useRouter()
466
716
 
467
- 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
468
722
 
469
- return value.router as Router
723
+ return router.matchRoute(rest as any, {
724
+ pending,
725
+ caseSensitive,
726
+ })
727
+ },
728
+ [],
729
+ )
470
730
  }
471
731
 
472
- export function useMatches(): RouteMatch[] {
473
- 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
474
743
  }
475
744
 
476
745
  export function Outlet() {
477
- const router = useRouter()
478
- const matches = useMatches().slice(1)
479
- const match = matches[0]
746
+ const matchIds = React.useContext(matchIdsContext).slice(1)
480
747
 
481
- const defaultPending = React.useCallback(() => null, [])
482
-
483
- if (!match) {
748
+ if (!matchIds[0]) {
484
749
  return null
485
750
  }
486
751
 
487
- 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 ??
488
764
  router.options.defaultPendingComponent ??
489
765
  defaultPending) as any
490
766
 
491
767
  const errorComponent =
492
- 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
493
778
 
494
779
  return (
495
- <MatchesProvider value={matches}>
496
- <React.Suspense fallback={<PendingComponent />}>
497
- <CatchBoundary errorComponent={errorComponent}>
498
- {
499
- ((): React.ReactNode => {
500
- if (match.status === 'error') {
501
- throw match.error
502
- }
503
-
504
- if (match.status === 'success') {
505
- return React.createElement(
506
- (match.__.component as any) ??
507
- router.options.defaultComponent ??
508
- Outlet,
509
- )
510
- }
511
-
512
- if (match.__.loadPromise) {
513
- console.log(match.matchId, 'suspend')
514
- throw match.__.loadPromise
515
- }
516
-
517
- invariant(false, 'This should never happen!')
518
- })() as JSX.Element
519
- }
520
- </CatchBoundary>
521
- </React.Suspense>
522
- </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>
523
802
  )
524
803
  }
525
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
+ [],
873
+ )
874
+ }
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
+
526
900
  class CatchBoundary extends React.Component<{
527
901
  children: any
528
902
  errorComponent: any
903
+ onCatch: (error: any, info: any) => void
529
904
  }> {
530
905
  state = {
531
906
  error: false,
907
+ info: undefined,
532
908
  }
533
909
  componentDidCatch(error: any, info: any) {
534
- console.error(error)
535
-
910
+ this.props.onCatch(error, info)
536
911
  this.setState({
537
912
  error,
538
913
  info,
539
914
  })
540
915
  }
541
916
  render() {
542
- 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)
543
942
 
544
- if (this.state.error) {
545
- 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)
546
956
  }
957
+ // props.reset()
958
+ }, [props.errorState.error])
547
959
 
548
- return this.props.children
960
+ if (props.errorState.error && activeErrorState.error) {
961
+ return React.createElement(errorComponent, activeErrorState)
549
962
  }
963
+
964
+ return props.children
550
965
  }
551
966
 
552
- 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
+
553
970
  return (
554
971
  <div style={{ padding: '.5rem', maxWidth: '100%' }}>
555
- <strong style={{ fontSize: '1.2rem' }}>Something went wrong!</strong>
556
- <div style={{ height: '.5rem' }} />
557
- <div>
558
- <pre>
559
- {error.message ? (
560
- <code
561
- style={{
562
- fontSize: '.7em',
563
- border: '1px solid red',
564
- borderRadius: '.25rem',
565
- padding: '.5rem',
566
- color: 'red',
567
- }}
568
- >
569
- {error.message}
570
- </code>
571
- ) : null}
572
- </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>
573
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}
574
1005
  </div>
575
1006
  )
576
1007
  }
577
1008
 
578
- export function usePrompt(message: string, when: boolean | any): void {
1009
+ export function useBlocker(
1010
+ message: string,
1011
+ condition: boolean | any = true,
1012
+ ): void {
579
1013
  const router = useRouter()
580
1014
 
581
1015
  React.useEffect(() => {
582
- if (!when) return
1016
+ if (!condition) return
583
1017
 
584
- let unblock = router.history.block((transition) => {
1018
+ let unblock = router.history.block((retry, cancel) => {
585
1019
  if (window.confirm(message)) {
586
1020
  unblock()
587
- transition.retry()
588
- } else {
589
- router.location.pathname = window.location.pathname
1021
+ retry()
590
1022
  }
591
1023
  })
592
1024
 
593
1025
  return unblock
594
- }, [when, location, message])
1026
+ })
595
1027
  }
596
1028
 
597
- export function Prompt({ message, when, children }: PromptProps) {
598
- usePrompt(message, when ?? true)
599
- 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
600
1062
  }