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

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