@tanstack/react-router 0.0.1-beta.5 → 0.0.1-beta.50

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,16 +1,11 @@
1
1
  import * as React from 'react'
2
2
 
3
- import { useSyncExternalStore } from 'use-sync-external-store/shim'
4
-
5
- import {
6
- AnyRoute,
7
- CheckId,
8
- rootRouteId,
9
- Router,
10
- RouterState,
11
- ToIdOption,
12
- } from '@tanstack/router-core'
13
3
  import {
4
+ Route,
5
+ RegisteredAllRouteInfo,
6
+ RegisteredRouter,
7
+ RouterStore,
8
+ last,
14
9
  warning,
15
10
  RouterOptions,
16
11
  RouteMatch,
@@ -20,10 +15,7 @@ import {
20
15
  AnyAllRouteInfo,
21
16
  DefaultAllRouteInfo,
22
17
  functionalUpdate,
23
- createRouter,
24
- AnyRouteInfo,
25
18
  AllRouteInfo,
26
- RouteInfo,
27
19
  ValidFromPath,
28
20
  LinkOptions,
29
21
  RouteInfoByPath,
@@ -31,111 +23,51 @@ import {
31
23
  NoInfer,
32
24
  ToOptions,
33
25
  invariant,
26
+ Router,
27
+ Expand,
28
+ Action,
29
+ ActionStore,
30
+ ActionSubmission,
34
31
  } from '@tanstack/router-core'
32
+ import { useStore } from './useStore'
33
+
34
+ //
35
35
 
36
36
  export * from '@tanstack/router-core'
37
37
 
38
- declare module '@tanstack/router-core' {
39
- interface FrameworkGenerics {
40
- Element: React.ReactNode
41
- // Any is required here so import() will work without having to do import().then(d => d.default)
42
- SyncOrAsyncElement: React.ReactNode | (() => Promise<any>)
43
- }
38
+ export { useStore }
44
39
 
45
- interface Router<
46
- TRouteConfig extends AnyRouteConfig = RouteConfig,
47
- TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
48
- > {
49
- useState: () => RouterState
50
- useRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
51
- routeId: TId,
52
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
53
- useMatch: <TId extends keyof TAllRouteInfo['routeInfoById']>(
54
- routeId: TId,
55
- ) => RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
56
- linkProps: <TTo extends string = '.'>(
57
- props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
58
- React.AnchorHTMLAttributes<HTMLAnchorElement>,
59
- ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
60
- Link: <TTo extends string = '.'>(
61
- props: LinkPropsOptions<TAllRouteInfo, '/', TTo> &
62
- React.AnchorHTMLAttributes<HTMLAnchorElement> &
63
- Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
64
- // 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
65
- children?:
66
- | React.ReactNode
67
- | ((state: { isActive: boolean }) => React.ReactNode)
68
- },
69
- ) => JSX.Element
70
- MatchRoute: <TTo extends string = '.'>(
71
- props: ToOptions<TAllRouteInfo, '/', TTo> &
72
- MatchRouteOptions & {
73
- // 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
74
- children?:
75
- | React.ReactNode
76
- | ((
77
- params: RouteInfoByPath<
78
- TAllRouteInfo,
79
- ResolveRelativePath<'/', NoInfer<TTo>>
80
- >['allParams'],
81
- ) => React.ReactNode)
82
- },
83
- ) => JSX.Element
84
- }
40
+ //
41
+
42
+ type ReactNode = any
43
+
44
+ export type SyncRouteComponent<TProps = {}> = (props: TProps) => ReactNode
45
+
46
+ export type RouteComponent<TProps = {}> = SyncRouteComponent<TProps> & {
47
+ preload?: () => Promise<void>
48
+ }
49
+
50
+ export function lazy(
51
+ importer: () => Promise<{ default: SyncRouteComponent }>,
52
+ ): RouteComponent {
53
+ const lazyComp = React.lazy(importer as any)
54
+ let preloaded: Promise<SyncRouteComponent>
55
+
56
+ const finalComp = lazyComp as unknown as RouteComponent
85
57
 
86
- interface Route<
87
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
88
- TRouteInfo extends AnyRouteInfo = RouteInfo,
89
- > {
90
- useRoute: <
91
- TTo extends string = '.',
92
- TResolved extends string = ResolveRelativePath<
93
- TRouteInfo['id'],
94
- NoInfer<TTo>
95
- >,
96
- >(
97
- routeId: CheckId<
98
- TAllRouteInfo,
99
- TResolved,
100
- ToIdOption<TAllRouteInfo, TRouteInfo['id'], TTo>
101
- >,
102
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TResolved]>
103
- linkProps: <TTo extends string = '.'>(
104
- props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
105
- React.AnchorHTMLAttributes<HTMLAnchorElement>,
106
- ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
107
- Link: <TTo extends string = '.'>(
108
- props: LinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
109
- React.AnchorHTMLAttributes<HTMLAnchorElement> &
110
- Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
111
- // 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
112
- children?:
113
- | React.ReactNode
114
- | ((state: { isActive: boolean }) => React.ReactNode)
115
- },
116
- ) => JSX.Element
117
- MatchRoute: <TTo extends string = '.'>(
118
- props: ToOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo> &
119
- MatchRouteOptions & {
120
- // 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
121
- children?:
122
- | React.ReactNode
123
- | ((
124
- params: RouteInfoByPath<
125
- TAllRouteInfo,
126
- ResolveRelativePath<TRouteInfo['fullPath'], NoInfer<TTo>>
127
- >['allParams'],
128
- ) => React.ReactNode)
129
- },
130
- ) => JSX.Element
58
+ finalComp.preload = async () => {
59
+ if (!preloaded) {
60
+ await importer()
61
+ }
131
62
  }
63
+
64
+ return finalComp
132
65
  }
133
66
 
134
- type LinkPropsOptions<
135
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
136
- TFrom extends ValidFromPath<TAllRouteInfo> = '/',
67
+ export type LinkPropsOptions<
68
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
137
69
  TTo extends string = '.',
138
- > = LinkOptions<TAllRouteInfo, TFrom, TTo> & {
70
+ > = LinkOptions<RegisteredAllRouteInfo, TFrom, TTo> & {
139
71
  // 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)
140
72
  activeProps?:
141
73
  | React.AnchorHTMLAttributes<HTMLAnchorElement>
@@ -146,396 +78,568 @@ type LinkPropsOptions<
146
78
  | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)
147
79
  }
148
80
 
81
+ export type MakeUseMatchRouteOptions<
82
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
83
+ TTo extends string = '.',
84
+ > = ToOptions<RegisteredAllRouteInfo, TFrom, TTo> & MatchRouteOptions
85
+
86
+ export type MakeMatchRouteOptions<
87
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
88
+ TTo extends string = '.',
89
+ > = ToOptions<RegisteredAllRouteInfo, TFrom, TTo> &
90
+ MatchRouteOptions & {
91
+ // 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
92
+ children?:
93
+ | ReactNode
94
+ | ((
95
+ params: RouteInfoByPath<
96
+ RegisteredAllRouteInfo,
97
+ ResolveRelativePath<TFrom, NoInfer<TTo>>
98
+ >['allParams'],
99
+ ) => ReactNode)
100
+ }
101
+
102
+ export type MakeLinkPropsOptions<
103
+ TFrom extends ValidFromPath<RegisteredAllRouteInfo> = '/',
104
+ TTo extends string = '.',
105
+ > = LinkPropsOptions<TFrom, TTo> & React.AnchorHTMLAttributes<HTMLAnchorElement>
106
+
107
+ export type MakeLinkOptions<
108
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
109
+ TTo extends string = '.',
110
+ > = LinkPropsOptions<TFrom, TTo> &
111
+ React.AnchorHTMLAttributes<HTMLAnchorElement> &
112
+ Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
113
+ // 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
114
+ children?: ReactNode | ((state: { isActive: boolean }) => ReactNode)
115
+ }
116
+
117
+ declare module '@tanstack/router-core' {
118
+ interface FrameworkGenerics {
119
+ Component: RouteComponent
120
+ ErrorComponent: RouteComponent<{
121
+ error: unknown
122
+ info: { componentStack: string }
123
+ }>
124
+ }
125
+
126
+ interface RouterOptions<TRouteConfig, TRouterContext> {
127
+ // ssrFooter?: () => JSX.Element | Node
128
+ }
129
+ }
130
+
149
131
  export type PromptProps = {
150
132
  message: string
151
133
  when?: boolean | any
152
- children?: React.ReactNode
134
+ children?: ReactNode
153
135
  }
154
136
 
155
137
  //
156
138
 
157
- const matchesContext = React.createContext<RouteMatch[]>(null!)
158
- const routerContext = React.createContext<{ router: Router<any, any> }>(null!)
139
+ export function useLinkProps<
140
+ TFrom extends ValidFromPath<RegisteredAllRouteInfo> = '/',
141
+ TTo extends string = '.',
142
+ >(
143
+ options: MakeLinkPropsOptions<TFrom, TTo>,
144
+ ): React.AnchorHTMLAttributes<HTMLAnchorElement> {
145
+ const router = useRouter()
146
+
147
+ const {
148
+ // custom props
149
+ type,
150
+ children,
151
+ target,
152
+ activeProps = () => ({ className: 'active' }),
153
+ inactiveProps = () => ({}),
154
+ activeOptions,
155
+ disabled,
156
+ // fromCurrent,
157
+ hash,
158
+ search,
159
+ params,
160
+ to = '.',
161
+ preload,
162
+ preloadDelay,
163
+ preloadMaxAge,
164
+ replace,
165
+ // element props
166
+ style,
167
+ className,
168
+ onClick,
169
+ onFocus,
170
+ onMouseEnter,
171
+ onMouseLeave,
172
+ onTouchStart,
173
+ onTouchEnd,
174
+ ...rest
175
+ } = options
176
+
177
+ const linkInfo = router.buildLink(options as any)
178
+
179
+ if (linkInfo.type === 'external') {
180
+ const { href } = linkInfo
181
+ return { href }
182
+ }
159
183
 
160
- // Detect if we're in the DOM
161
- const isDOM = Boolean(
162
- typeof window !== 'undefined' &&
163
- window.document &&
164
- window.document.createElement,
165
- )
184
+ const { handleClick, handleFocus, handleEnter, handleLeave, isActive, next } =
185
+ linkInfo
186
+
187
+ const reactHandleClick = (e: Event) => {
188
+ if (React.startTransition) {
189
+ // This is a hack for react < 18
190
+ React.startTransition(() => {
191
+ handleClick(e)
192
+ })
193
+ } else {
194
+ handleClick(e)
195
+ }
196
+ }
166
197
 
167
- const useLayoutEffect = isDOM ? React.useLayoutEffect : React.useEffect
198
+ const composeHandlers =
199
+ (handlers: (undefined | ((e: any) => void))[]) =>
200
+ (e: React.SyntheticEvent) => {
201
+ if (e.persist) e.persist()
202
+ handlers.filter(Boolean).forEach((handler) => {
203
+ if (e.defaultPrevented) return
204
+ handler!(e)
205
+ })
206
+ }
168
207
 
169
- export type MatchesProviderProps = {
170
- value: RouteMatch[]
171
- children: React.ReactNode
208
+ // Get the active props
209
+ const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> = isActive
210
+ ? functionalUpdate(activeProps as any, {}) ?? {}
211
+ : {}
212
+
213
+ // Get the inactive props
214
+ const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
215
+ isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
216
+
217
+ return {
218
+ ...resolvedActiveProps,
219
+ ...resolvedInactiveProps,
220
+ ...rest,
221
+ href: disabled ? undefined : next.href,
222
+ onClick: composeHandlers([onClick, reactHandleClick]),
223
+ onFocus: composeHandlers([onFocus, handleFocus]),
224
+ onMouseEnter: composeHandlers([onMouseEnter, handleEnter]),
225
+ onMouseLeave: composeHandlers([onMouseLeave, handleLeave]),
226
+ target,
227
+ style: {
228
+ ...style,
229
+ ...resolvedActiveProps.style,
230
+ ...resolvedInactiveProps.style,
231
+ },
232
+ className:
233
+ [
234
+ className,
235
+ resolvedActiveProps.className,
236
+ resolvedInactiveProps.className,
237
+ ]
238
+ .filter(Boolean)
239
+ .join(' ') || undefined,
240
+ ...(disabled
241
+ ? {
242
+ role: 'link',
243
+ 'aria-disabled': true,
244
+ }
245
+ : undefined),
246
+ ['data-status']: isActive ? 'active' : undefined,
247
+ }
172
248
  }
173
249
 
174
- export function MatchesProvider(props: MatchesProviderProps) {
175
- return <matchesContext.Provider {...props} />
250
+ export interface LinkFn<
251
+ TDefaultFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
252
+ TDefaultTo extends string = '.',
253
+ > {
254
+ <
255
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = TDefaultFrom,
256
+ TTo extends string = TDefaultTo,
257
+ >(
258
+ props: MakeLinkOptions<TFrom, TTo> & React.RefAttributes<HTMLAnchorElement>,
259
+ ): ReactNode
176
260
  }
177
261
 
178
- const useRouterSubscription = (router: Router<any, any>) => {
179
- useSyncExternalStore(
180
- (cb) => router.subscribe(() => cb()),
181
- () => router.state,
182
- () => router.state,
262
+ export const Link: LinkFn = React.forwardRef((props: any, ref) => {
263
+ const linkProps = useLinkProps(props)
264
+
265
+ return (
266
+ <a
267
+ {...{
268
+ ref: ref as any,
269
+ ...linkProps,
270
+ children:
271
+ typeof props.children === 'function'
272
+ ? props.children({
273
+ isActive: (linkProps as any)['data-status'] === 'active',
274
+ })
275
+ : props.children,
276
+ }}
277
+ />
183
278
  )
279
+ }) as any
280
+
281
+ type MatchesContextValue = RouteMatch[]
282
+
283
+ export const matchesContext = React.createContext<MatchesContextValue>(null!)
284
+ export const routerContext = React.createContext<{ router: RegisteredRouter }>(
285
+ null!,
286
+ )
287
+
288
+ export type MatchesProviderProps = {
289
+ value: MatchesContextValue
290
+ children: ReactNode
184
291
  }
185
292
 
186
- export function createReactRouter<
293
+ export class ReactRouter<
187
294
  TRouteConfig extends AnyRouteConfig = RouteConfig,
188
- >(opts: RouterOptions<TRouteConfig>): Router<TRouteConfig> {
189
- const makeRouteExt = (
190
- route: AnyRoute,
191
- router: Router<any, any>,
192
- ): Pick<AnyRoute, 'useRoute' | 'linkProps' | 'Link' | 'MatchRoute'> => {
193
- return {
194
- useRoute: (subRouteId = '.' as any) => {
195
- const resolvedRouteId = router.resolvePath(
196
- route.routeId,
197
- subRouteId as string,
198
- )
199
- const resolvedRoute = router.getRoute(resolvedRouteId)
200
- useRouterSubscription(router)
201
- invariant(
202
- resolvedRoute,
203
- `Could not find a route for route "${
204
- resolvedRouteId as string
205
- }"! Did you forget to add it to your route config?`,
206
- )
207
- return resolvedRoute
208
- },
209
- linkProps: (options) => {
210
- const {
211
- // custom props
212
- type,
213
- children,
214
- target,
215
- activeProps = () => ({ className: 'active' }),
216
- inactiveProps = () => ({}),
217
- activeOptions,
218
- disabled,
219
- // fromCurrent,
220
- hash,
221
- search,
222
- params,
223
- to,
224
- preload,
225
- preloadDelay,
226
- preloadMaxAge,
227
- replace,
228
- // element props
229
- style,
230
- className,
231
- onClick,
232
- onFocus,
233
- onMouseEnter,
234
- onMouseLeave,
235
- onTouchStart,
236
- onTouchEnd,
237
- ...rest
238
- } = options
239
-
240
- const linkInfo = route.buildLink(options)
241
-
242
- if (linkInfo.type === 'external') {
243
- const { href } = linkInfo
244
- return { href }
295
+ TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
296
+ TRouterContext = unknown,
297
+ > extends Router<TRouteConfig, TAllRouteInfo, TRouterContext> {
298
+ constructor(opts: RouterOptions<TRouteConfig, TRouterContext>) {
299
+ super({
300
+ ...opts,
301
+ loadComponent: async (component) => {
302
+ if (component.preload) {
303
+ await component.preload()
245
304
  }
246
305
 
247
- const {
248
- handleClick,
249
- handleFocus,
250
- handleEnter,
251
- handleLeave,
252
- isActive,
253
- next,
254
- } = linkInfo
255
-
256
- const composeHandlers =
257
- (handlers: (undefined | ((e: any) => void))[]) =>
258
- (e: React.SyntheticEvent) => {
259
- e.persist()
260
- handlers.forEach((handler) => {
261
- if (handler) handler(e)
262
- })
263
- }
264
-
265
- // Get the active props
266
- const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> =
267
- isActive ? functionalUpdate(activeProps, {}) ?? {} : {}
268
-
269
- // Get the inactive props
270
- const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
271
- isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
272
-
273
- return {
274
- ...resolvedActiveProps,
275
- ...resolvedInactiveProps,
276
- ...rest,
277
- href: disabled ? undefined : next.href,
278
- onClick: composeHandlers([handleClick, onClick]),
279
- onFocus: composeHandlers([handleFocus, onFocus]),
280
- onMouseEnter: composeHandlers([handleEnter, onMouseEnter]),
281
- onMouseLeave: composeHandlers([handleLeave, onMouseLeave]),
282
- target,
283
- style: {
284
- ...style,
285
- ...resolvedActiveProps.style,
286
- ...resolvedInactiveProps.style,
287
- },
288
- className:
289
- [
290
- className,
291
- resolvedActiveProps.className,
292
- resolvedInactiveProps.className,
293
- ]
294
- .filter(Boolean)
295
- .join(' ') || undefined,
296
- ...(disabled
297
- ? {
298
- role: 'link',
299
- 'aria-disabled': true,
300
- }
301
- : undefined),
302
- ['data-status']: isActive ? 'active' : undefined,
303
- }
306
+ return component as any
304
307
  },
305
- Link: React.forwardRef((props: any, ref) => {
306
- const linkProps = route.linkProps(props)
307
-
308
- useRouterSubscription(router)
309
-
310
- return (
311
- <a
312
- {...{
313
- ref: ref as any,
314
- ...linkProps,
315
- children:
316
- typeof props.children === 'function'
317
- ? props.children({
318
- isActive: (linkProps as any)['data-status'] === 'active',
319
- })
320
- : props.children,
321
- }}
322
- />
323
- )
324
- }) as any,
325
- MatchRoute: (opts) => {
326
- const { pending, caseSensitive, children, ...rest } = opts
327
-
328
- const params = route.matchRoute(rest as any, {
329
- pending,
330
- caseSensitive,
331
- })
332
-
333
- if (!params) {
334
- return null
335
- }
336
-
337
- return typeof opts.children === 'function'
338
- ? opts.children(params as any)
339
- : (opts.children as any)
340
- },
341
- }
308
+ })
342
309
  }
343
-
344
- const coreRouter = createRouter<TRouteConfig>({
345
- ...opts,
346
- createRouter: (router) => {
347
- const routerExt: Pick<Router<any, any>, 'useMatch' | 'useState'> = {
348
- useState: () => {
349
- useRouterSubscription(router)
350
- return router.state
351
- },
352
- useMatch: (routeId) => {
353
- useRouterSubscription(router)
354
-
355
- invariant(
356
- routeId !== rootRouteId,
357
- `"${rootRouteId}" cannot be used with useMatch! Did you mean to useRoute("${rootRouteId}")?`,
358
- )
359
-
360
- const runtimeMatch = useMatch()
361
- const match = router.state.matches.find((d) => d.routeId === routeId)
362
-
363
- invariant(
364
- match,
365
- `Could not find a match for route "${
366
- routeId as string
367
- }" being rendered in this component!`,
368
- )
369
-
370
- invariant(
371
- runtimeMatch.routeId == match?.routeId,
372
- `useMatch('${
373
- match?.routeId as string
374
- }') is being called in a component that is meant to render the '${
375
- runtimeMatch.routeId
376
- }' route. Did you mean to 'useRoute(${
377
- match?.routeId as string
378
- })' instead?`,
379
- )
380
-
381
- if (!match) {
382
- invariant('Match not found!')
383
- }
384
-
385
- return match
386
- },
387
- }
388
-
389
- const routeExt = makeRouteExt(router.getRoute('/'), router)
390
-
391
- Object.assign(router, routerExt, routeExt)
392
- },
393
- createRoute: ({ router, route }) => {
394
- const routeExt = makeRouteExt(route, router)
395
-
396
- Object.assign(route, routeExt)
397
- },
398
- createElement: async (element) => {
399
- if (typeof element === 'function') {
400
- const res = (await element()) as any
401
-
402
- // Support direct import() calls
403
- if (typeof res === 'object' && res.default) {
404
- return React.createElement(res.default)
405
- } else {
406
- return res
407
- }
408
- }
409
-
410
- return element
411
- },
412
- })
413
-
414
- return coreRouter as any
415
310
  }
416
311
 
417
312
  export type RouterProps<
418
313
  TRouteConfig extends AnyRouteConfig = RouteConfig,
419
314
  TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
420
- > = RouterOptions<TRouteConfig> & {
421
- router: Router<TRouteConfig, TAllRouteInfo>
422
- // Children will default to `<Outlet />` if not provided
423
- children?: React.ReactNode
315
+ TRouterContext = unknown,
316
+ > = RouterOptions<TRouteConfig, TRouterContext> & {
317
+ router: Router<TRouteConfig, TAllRouteInfo, TRouterContext>
424
318
  }
425
319
 
426
320
  export function RouterProvider<
427
321
  TRouteConfig extends AnyRouteConfig = RouteConfig,
428
322
  TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
429
- >({ children, router, ...rest }: RouterProps<TRouteConfig, TAllRouteInfo>) {
323
+ TRouterContext = unknown,
324
+ >({
325
+ router,
326
+ ...rest
327
+ }: RouterProps<TRouteConfig, TAllRouteInfo, TRouterContext>) {
430
328
  router.update(rest)
431
329
 
432
- useRouterSubscription(router)
330
+ const [, , currentMatches] = useStore(
331
+ router.store,
332
+ (s) => [s.status, s.pendingMatches, s.currentMatches],
333
+ true,
334
+ )
433
335
 
434
- useLayoutEffect(() => {
435
- return router.mount()
436
- }, [router])
336
+ React.useEffect(router.mount, [router])
437
337
 
438
338
  return (
439
- <routerContext.Provider value={{ router }}>
440
- <MatchesProvider value={router.state.matches}>
441
- {children ?? <Outlet />}
442
- </MatchesProvider>
443
- </routerContext.Provider>
339
+ <>
340
+ <routerContext.Provider value={{ router: router as any }}>
341
+ <matchesContext.Provider value={[undefined!, ...currentMatches]}>
342
+ <Outlet />
343
+ </matchesContext.Provider>
344
+ </routerContext.Provider>
345
+ </>
444
346
  )
445
347
  }
446
348
 
447
- function useRouter(): Router {
349
+ export function useRouter(): RegisteredRouter {
448
350
  const value = React.useContext(routerContext)
449
351
  warning(!value, 'useRouter must be used inside a <Router> component!')
352
+ return value.router
353
+ }
450
354
 
451
- useRouterSubscription(value.router)
452
-
453
- return value.router as Router
355
+ export function useRouterStore<T = RouterStore>(
356
+ selector?: (state: Router['store']) => T,
357
+ shallow?: boolean,
358
+ ): T {
359
+ const router = useRouter()
360
+ return useStore(router.store, selector as any, shallow)
454
361
  }
455
362
 
456
- function useMatches(): RouteMatch[] {
363
+ export function useMatches(): RouteMatch[] {
457
364
  return React.useContext(matchesContext)
458
365
  }
459
366
 
460
- // function useParentMatches(): RouteMatch[] {
461
- // const router = useRouter()
462
- // const match = useMatch()
463
- // const matches = router.state.matches
464
- // return matches.slice(
465
- // 0,
466
- // matches.findIndex((d) => d.matchId === match.matchId) - 1,
467
- // )
468
- // }
367
+ export function useMatch<
368
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'],
369
+ TStrict extends boolean = true,
370
+ TRouteMatch = RouteMatch<
371
+ RegisteredAllRouteInfo,
372
+ RegisteredAllRouteInfo['routeInfoById'][TFrom]
373
+ >,
374
+ >(opts?: {
375
+ from: TFrom
376
+ strict?: TStrict
377
+ track?: (match: TRouteMatch) => any
378
+ shallow?: boolean
379
+ }): TStrict extends true ? TRouteMatch : TRouteMatch | undefined {
380
+ const router = useRouter()
381
+ const nearestMatch = useMatches()[0]!
382
+ const match = opts?.from
383
+ ? router.store.state.currentMatches.find((d) => d.route.id === opts?.from)
384
+ : nearestMatch
385
+
386
+ invariant(
387
+ match,
388
+ `Could not find ${
389
+ opts?.from ? `an active match from "${opts.from}"` : 'a nearest match!'
390
+ }`,
391
+ )
392
+
393
+ if (opts?.strict ?? true) {
394
+ invariant(
395
+ nearestMatch.route.id == match?.route.id,
396
+ `useMatch("${
397
+ match?.route.id as string
398
+ }") is being called in a component that is meant to render the '${
399
+ nearestMatch.route.id
400
+ }' route. Did you mean to 'useMatch("${
401
+ match?.route.id as string
402
+ }", { strict: false })' or 'useRoute("${
403
+ match?.route.id as string
404
+ }")' instead?`,
405
+ )
406
+ }
469
407
 
470
- function useMatch<T>(): RouteMatch {
471
- return useMatches()?.[0] as RouteMatch
408
+ useStore(
409
+ match!.store,
410
+ (d) => opts?.track?.(match as any) ?? match,
411
+ opts?.shallow,
412
+ )
413
+
414
+ return match as any
472
415
  }
473
416
 
474
- export function Outlet() {
417
+ export function useRoute<
418
+ TId extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
419
+ >(
420
+ routeId: TId,
421
+ ): Route<RegisteredAllRouteInfo, RegisteredAllRouteInfo['routeInfoById'][TId]> {
475
422
  const router = useRouter()
476
- const [, ...matches] = useMatches()
423
+ const resolvedRoute = router.getRoute(routeId as any)
477
424
 
478
- const childMatch = matches[0]
425
+ invariant(
426
+ resolvedRoute,
427
+ `Could not find a route for route "${
428
+ routeId as string
429
+ }"! Did you forget to add it to your route config?`,
430
+ )
479
431
 
480
- if (!childMatch) return null
432
+ return resolvedRoute as any
433
+ }
481
434
 
482
- const element = ((): React.ReactNode => {
483
- if (!childMatch) {
484
- return null
485
- }
435
+ export function useLoaderData<
436
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
437
+ TStrict extends boolean = true,
438
+ TLoaderData = RegisteredAllRouteInfo['routeInfoById'][TFrom]['loaderData'],
439
+ >(opts?: {
440
+ from: TFrom
441
+ strict?: TStrict
442
+ track?: (loaderData: TLoaderData) => any
443
+ }): TStrict extends true ? TLoaderData : TLoaderData | undefined {
444
+ const match = useMatch(opts)
445
+
446
+ invariant(
447
+ match,
448
+ `Could not find ${
449
+ opts?.from ? `an active match from "${opts.from}"` : 'a nearest match!'
450
+ }`,
451
+ )
486
452
 
487
- const errorElement =
488
- childMatch.__.errorElement ?? router.options.defaultErrorElement
453
+ useStore(
454
+ (match as any).store,
455
+ (d: any) => opts?.track?.(d.loaderData) ?? d.loaderData,
456
+ )
489
457
 
490
- if (childMatch.status === 'error') {
491
- if (errorElement) {
492
- return errorElement as any
493
- }
458
+ return (match as unknown as RouteMatch).store.state.loaderData as any
459
+ }
494
460
 
495
- if (
496
- childMatch.options.useErrorBoundary ||
497
- router.options.useErrorBoundary
498
- ) {
499
- throw childMatch.error
500
- }
461
+ export function useSearch<
462
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'],
463
+ TStrict extends boolean = true,
464
+ TSearch = RegisteredAllRouteInfo['routeInfoById'][TFrom]['fullSearchSchema'],
465
+ TSelected = TSearch,
466
+ >(opts?: {
467
+ from: TFrom
468
+ strict?: TStrict
469
+ track?: (search: TSearch) => TSelected
470
+ }): TStrict extends true ? TSelected : TSelected | undefined {
471
+ const match = useMatch(opts)
472
+ useStore(
473
+ (match as any).store,
474
+ (d: any) => opts?.track?.(d.search) ?? d.search,
475
+ )
501
476
 
502
- return <DefaultErrorBoundary error={childMatch.error} />
503
- }
477
+ return (match as unknown as RouteMatch).store.state.search as any
478
+ }
504
479
 
505
- if (childMatch.status === 'loading' || childMatch.status === 'idle') {
506
- if (childMatch.isPending) {
507
- const pendingElement =
508
- childMatch.__.pendingElement ?? router.options.defaultPendingElement
480
+ export function useParams<
481
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
482
+ TDefaultSelected = Expand<
483
+ RegisteredAllRouteInfo['allParams'] &
484
+ RegisteredAllRouteInfo['routeInfoById'][TFrom]['allParams']
485
+ >,
486
+ TSelected = TDefaultSelected,
487
+ >(opts?: {
488
+ from: TFrom
489
+ track?: (search: TDefaultSelected) => TSelected
490
+ }): TSelected {
491
+ const router = useRouter()
492
+ useStore(router.store, (d) => {
493
+ const params = last(d.currentMatches)?.params as any
494
+ return opts?.track?.(params) ?? params
495
+ })
509
496
 
510
- if (childMatch.options.pendingMs || pendingElement) {
511
- return (pendingElement as any) ?? null
512
- }
513
- }
497
+ return last(router.store.state.currentMatches)?.params as any
498
+ }
499
+
500
+ export function useNavigate<
501
+ TDefaultFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
502
+ >(defaultOpts: { from?: TDefaultFrom }) {
503
+ const router = useRouter()
504
+ return <
505
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = TDefaultFrom,
506
+ TTo extends string = '.',
507
+ >(
508
+ opts: MakeLinkOptions<TFrom, TTo>,
509
+ ) => {
510
+ return router.navigate({ ...defaultOpts, ...(opts as any) })
511
+ }
512
+ }
513
+
514
+ export function useMatchRoute() {
515
+ const router = useRouter()
516
+
517
+ return <
518
+ TFrom extends ValidFromPath<RegisteredAllRouteInfo> = '/',
519
+ TTo extends string = '.',
520
+ >(
521
+ opts: MakeUseMatchRouteOptions<TFrom, TTo>,
522
+ ) => {
523
+ const { pending, caseSensitive, ...rest } = opts
524
+
525
+ return router.matchRoute(rest as any, {
526
+ pending,
527
+ caseSensitive,
528
+ })
529
+ }
530
+ }
531
+
532
+ export function MatchRoute<
533
+ TFrom extends ValidFromPath<RegisteredAllRouteInfo> = '/',
534
+ TTo extends string = '.',
535
+ >(props: MakeMatchRouteOptions<TFrom, TTo>): any {
536
+ const matchRoute = useMatchRoute()
537
+ const params = matchRoute(props)
538
+
539
+ if (!params) {
540
+ return null
541
+ }
542
+
543
+ if (typeof props.children === 'function') {
544
+ return (props.children as any)(params)
545
+ }
546
+
547
+ return params ? props.children : null
548
+ }
514
549
 
515
- return null
550
+ export function Outlet() {
551
+ const matches = useMatches().slice(1)
552
+ const match = matches[0]
553
+
554
+ if (!match) {
555
+ return null
556
+ }
557
+
558
+ return <SubOutlet matches={matches} match={match} />
559
+ }
560
+
561
+ function SubOutlet({
562
+ matches,
563
+ match,
564
+ }: {
565
+ matches: RouteMatch[]
566
+ match: RouteMatch
567
+ }) {
568
+ const router = useRouter()
569
+ useStore(match!.store)
570
+
571
+ const defaultPending = React.useCallback(() => null, [])
572
+
573
+ const Inner = React.useCallback((props: { match: RouteMatch }): any => {
574
+ if (props.match.store.state.status === 'error') {
575
+ throw props.match.store.state.error
576
+ }
577
+
578
+ if (props.match.store.state.status === 'success') {
579
+ return React.createElement(
580
+ (props.match.component as any) ??
581
+ router.options.defaultComponent ??
582
+ Outlet,
583
+ )
584
+ }
585
+
586
+ if (props.match.store.state.status === 'loading') {
587
+ throw props.match.__loadPromise
516
588
  }
517
589
 
518
- return (childMatch.__.element as any) ?? router.options.defaultElement
519
- })() as JSX.Element
590
+ invariant(
591
+ false,
592
+ 'Idle routeMatch status encountered during rendering! You should never see this. File an issue!',
593
+ )
594
+ }, [])
520
595
 
521
- const catchElement =
522
- childMatch?.options.catchElement ?? router.options.defaultCatchElement
596
+ const PendingComponent = (match.pendingComponent ??
597
+ router.options.defaultPendingComponent ??
598
+ defaultPending) as any
599
+
600
+ const errorComponent =
601
+ match.errorComponent ?? router.options.defaultErrorComponent
523
602
 
524
603
  return (
525
- <MatchesProvider value={matches} key={childMatch.matchId}>
526
- <CatchBoundary catchElement={catchElement}>{element}</CatchBoundary>
527
- </MatchesProvider>
604
+ <matchesContext.Provider value={matches}>
605
+ <React.Suspense fallback={<PendingComponent />}>
606
+ <CatchBoundary
607
+ key={match.route.id}
608
+ errorComponent={errorComponent}
609
+ match={match as any}
610
+ >
611
+ <Inner match={match} />
612
+ </CatchBoundary>
613
+ </React.Suspense>
614
+ {/* Provide a suffix suspense boundary to make sure the router is
615
+ ready to be dehydrated on the server */}
616
+ {/* {router.options.ssrFooter && match.matchId === rootRouteId ? (
617
+ <React.Suspense fallback={null}>
618
+ {(() => {
619
+ if (router.store.pending) {
620
+ throw router.navigationPromise
621
+ }
622
+
623
+ return router.options.ssrFooter()
624
+ })()}
625
+ </React.Suspense>
626
+ ) : null} */}
627
+ </matchesContext.Provider>
528
628
  )
529
629
  }
530
630
 
531
631
  class CatchBoundary extends React.Component<{
532
632
  children: any
533
- catchElement: any
633
+ errorComponent: any
634
+ match: RouteMatch
534
635
  }> {
535
636
  state = {
536
637
  error: false,
638
+ info: undefined,
537
639
  }
640
+
538
641
  componentDidCatch(error: any, info: any) {
642
+ console.error(`Error in route match: ${this.props.match.id}`)
539
643
  console.error(error)
540
644
 
541
645
  this.setState({
@@ -543,23 +647,63 @@ class CatchBoundary extends React.Component<{
543
647
  info,
544
648
  })
545
649
  }
546
- reset = () => {
547
- this.setState({
548
- error: false,
549
- info: false,
550
- })
551
- }
650
+
552
651
  render() {
553
- const catchElement = this.props.catchElement ?? DefaultErrorBoundary
652
+ return (
653
+ <CatchBoundaryInner
654
+ {...this.props}
655
+ errorState={this.state}
656
+ reset={() => this.setState({})}
657
+ />
658
+ )
659
+ }
660
+ }
661
+
662
+ // This is the messiest thing ever... I'm either seriously tired (likely) or
663
+ // there has to be a better way to reset error boundaries when the
664
+ // router's location key changes.
665
+ function CatchBoundaryInner(props: {
666
+ children: any
667
+ errorComponent: any
668
+ errorState: { error: unknown; info: any }
669
+ reset: () => void
670
+ }) {
671
+ const [activeErrorState, setActiveErrorState] = React.useState(
672
+ props.errorState,
673
+ )
674
+ const router = useRouter()
675
+ const errorComponent = props.errorComponent ?? DefaultErrorBoundary
676
+
677
+ // React.useEffect(() => {
678
+ // if (activeErrorState) {
679
+ // let prevKey = router.store.currentLocation.key
680
+ // return createRoot((dispose) => {
681
+ // createEffect(() => {
682
+ // if (router.store.currentLocation.key !== prevKey) {
683
+ // prevKey = router.store.currentLocation.key
684
+ // setActiveErrorState({} as any)
685
+ // }
686
+ // })
687
+
688
+ // return dispose
689
+ // })
690
+ // }
691
+
692
+ // return
693
+ // }, [activeErrorState])
554
694
 
555
- if (this.state.error) {
556
- return typeof catchElement === 'function'
557
- ? catchElement(this.state)
558
- : catchElement
695
+ React.useEffect(() => {
696
+ if (props.errorState.error) {
697
+ setActiveErrorState(props.errorState)
559
698
  }
699
+ props.reset()
700
+ }, [props.errorState.error])
560
701
 
561
- return this.props.children
702
+ if (props.errorState.error) {
703
+ return React.createElement(errorComponent, activeErrorState)
562
704
  }
705
+
706
+ return props.children
563
707
  }
564
708
 
565
709
  export function DefaultErrorBoundary({ error }: { error: any }) {
@@ -584,43 +728,65 @@ export function DefaultErrorBoundary({ error }: { error: any }) {
584
728
  ) : null}
585
729
  </pre>
586
730
  </div>
587
- <div style={{ height: '1rem' }} />
588
- <div
589
- style={{
590
- fontSize: '.8em',
591
- borderLeft: '3px solid rgba(127, 127, 127, 1)',
592
- paddingLeft: '.5rem',
593
- opacity: 0.5,
594
- }}
595
- >
596
- If you are the owner of this website, it's highly recommended that you
597
- configure your own custom Catch/Error boundaries for the router. You can
598
- optionally configure a boundary for each route.
599
- </div>
600
731
  </div>
601
732
  )
602
733
  }
603
734
 
604
- export function usePrompt(message: string, when: boolean | any): void {
605
- const router = useRouter()
606
-
607
- React.useEffect(() => {
608
- if (!when) return
609
-
610
- let unblock = router.history.block((transition) => {
611
- if (window.confirm(message)) {
612
- unblock()
613
- transition.retry()
614
- } else {
615
- router.location.pathname = window.location.pathname
616
- }
617
- })
735
+ export function useAction<
736
+ TKey extends string = string,
737
+ TPayload = unknown,
738
+ TResponse = unknown,
739
+ TError = Error,
740
+ >(
741
+ action: Action<TKey, TPayload, TResponse, TError>,
742
+ opts?: {
743
+ track?: (actionStore: ActionStore<TPayload, TResponse, TError>) => any
744
+ },
745
+ ): Action & {
746
+ latestSubmission: ActionSubmission<TPayload, TResponse, TError>
747
+ pendingSubmissions: ActionSubmission<TPayload, TResponse, TError>[]
748
+ } {
749
+ useStore(action.store, (d) => opts?.track?.(d) ?? d, true)
750
+
751
+ const [ref] = React.useState({})
752
+
753
+ Object.assign(ref, {
754
+ ...action,
755
+ latestSubmission:
756
+ action.store.state.submissions[action.store.state.submissions.length - 1],
757
+ pendingSubmissions: React.useMemo(
758
+ () =>
759
+ action.store.state.submissions.filter((d) => d.status === 'pending'),
760
+ [action.store.state.submissions],
761
+ ),
762
+ })
618
763
 
619
- return unblock
620
- }, [when, location, message])
764
+ return ref as any
621
765
  }
622
766
 
623
- export function Prompt({ message, when, children }: PromptProps) {
624
- usePrompt(message, when ?? true)
625
- return (children ?? null) as React.ReactNode
626
- }
767
+ // TODO: While we migrate away from the history package, these need to be disabled
768
+ // export function usePrompt(message: string, when: boolean | any): void {
769
+ // const router = useRouter()
770
+
771
+ // React.useEffect(() => {
772
+ // if (!when) return
773
+
774
+ // let unblock = router.getHistory().block((transition) => {
775
+ // if (window.confirm(message)) {
776
+ // unblock()
777
+ // transition.retry()
778
+ // } else {
779
+ // router.setStore((s) => {
780
+ // s.currentLocation.pathname = window.location.pathname
781
+ // })
782
+ // }
783
+ // })
784
+
785
+ // return unblock
786
+ // }, [when, message])
787
+ // }
788
+
789
+ // export function Prompt({ message, when, children }: PromptProps) {
790
+ // usePrompt(message, when ?? true)
791
+ // return (children ?? null) as ReactNode
792
+ // }