@tanstack/react-router 0.0.1-beta.16 → 0.0.1-beta.160

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