@tanstack/react-router 0.0.1-beta.38 → 0.0.1-beta.39

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,19 +1,19 @@
1
1
  import * as React from 'react'
2
2
 
3
3
  import { useSyncExternalStore } from 'use-sync-external-store/shim'
4
+ // @ts-ignore
5
+ // import { useSyncExternalStore } from './uSES/useSyncExternalStoreShim'
6
+ import { createEffect, createRoot, untrack, unwrap } from '@solidjs/reactivity'
7
+ import { createStore } from '@solidjs/reactivity'
4
8
 
5
9
  import {
6
- AnyRoute,
7
- CheckId,
8
- rootRouteId,
9
10
  Route,
10
11
  RegisteredAllRouteInfo,
11
12
  RegisteredRouter,
12
- RouterState,
13
- ToIdOption,
13
+ RouterStore,
14
14
  last,
15
- } from '@tanstack/router-core'
16
- import {
15
+ sharedClone,
16
+ Action,
17
17
  warning,
18
18
  RouterOptions,
19
19
  RouteMatch,
@@ -24,9 +24,7 @@ import {
24
24
  DefaultAllRouteInfo,
25
25
  functionalUpdate,
26
26
  createRouter,
27
- AnyRouteInfo,
28
27
  AllRouteInfo,
29
- RouteInfo,
30
28
  ValidFromPath,
31
29
  LinkOptions,
32
30
  RouteInfoByPath,
@@ -35,54 +33,42 @@ import {
35
33
  ToOptions,
36
34
  invariant,
37
35
  Router,
36
+ Expand,
38
37
  } from '@tanstack/router-core'
39
38
 
40
39
  export * from '@tanstack/router-core'
41
40
 
42
- export type SyncRouteComponent<TProps = {}> = (
43
- props: TProps,
44
- ) => JSX.Element | React.ReactNode
41
+ export * from '@solidjs/reactivity'
42
+
43
+ type ReactNode = any
44
+
45
+ export type SyncRouteComponent<TProps = {}> = (props: TProps) => ReactNode
45
46
 
46
47
  export type RouteComponent<TProps = {}> = SyncRouteComponent<TProps> & {
47
- preload?: () => Promise<SyncRouteComponent<TProps>>
48
+ preload?: () => Promise<void>
48
49
  }
49
50
 
50
51
  export function lazy(
51
52
  importer: () => Promise<{ default: SyncRouteComponent }>,
52
53
  ): RouteComponent {
53
54
  const lazyComp = React.lazy(importer as any)
54
- let promise: Promise<SyncRouteComponent>
55
- let resolvedComp: SyncRouteComponent
56
-
57
- const forwardedComp = React.forwardRef((props, ref) => {
58
- const resolvedCompRef = React.useRef(resolvedComp || lazyComp)
59
- return React.createElement(
60
- resolvedCompRef.current as any,
61
- { ...(ref ? { ref } : {}), ...props } as any,
62
- )
63
- })
55
+ let preloaded: Promise<SyncRouteComponent>
64
56
 
65
- const finalComp = forwardedComp as unknown as RouteComponent
57
+ const finalComp = lazyComp as unknown as RouteComponent
66
58
 
67
- finalComp.preload = () => {
68
- if (!promise) {
69
- promise = importer().then((module) => {
70
- resolvedComp = module.default
71
- return resolvedComp
72
- })
59
+ finalComp.preload = async () => {
60
+ if (!preloaded) {
61
+ await importer()
73
62
  }
74
-
75
- return promise
76
63
  }
77
64
 
78
65
  return finalComp
79
66
  }
80
67
 
81
68
  export type LinkPropsOptions<
82
- TAllRouteInfo extends AnyAllRouteInfo,
83
- TFrom extends ValidFromPath<TAllRouteInfo>,
84
- TTo extends string,
85
- > = LinkOptions<TAllRouteInfo, TFrom, TTo> & {
69
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
70
+ TTo extends string = '.',
71
+ > = LinkOptions<RegisteredAllRouteInfo, TFrom, TTo> & {
86
72
  // 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)
87
73
  activeProps?:
88
74
  | React.AnchorHTMLAttributes<HTMLAnchorElement>
@@ -93,41 +79,40 @@ export type LinkPropsOptions<
93
79
  | (() => React.AnchorHTMLAttributes<HTMLAnchorElement>)
94
80
  }
95
81
 
82
+ export type MakeUseMatchRouteOptions<
83
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
84
+ TTo extends string = '.',
85
+ > = ToOptions<RegisteredAllRouteInfo, TFrom, TTo> & MatchRouteOptions
86
+
96
87
  export type MakeMatchRouteOptions<
97
- TAllRouteInfo extends AnyAllRouteInfo,
98
- TFrom extends ValidFromPath<TAllRouteInfo>,
99
- TTo extends string,
100
- > = ToOptions<TAllRouteInfo, TFrom, TTo> &
88
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
89
+ TTo extends string = '.',
90
+ > = ToOptions<RegisteredAllRouteInfo, TFrom, TTo> &
101
91
  MatchRouteOptions & {
102
92
  // 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
103
93
  children?:
104
- | React.ReactNode
94
+ | ReactNode
105
95
  | ((
106
96
  params: RouteInfoByPath<
107
- TAllRouteInfo,
97
+ RegisteredAllRouteInfo,
108
98
  ResolveRelativePath<TFrom, NoInfer<TTo>>
109
99
  >['allParams'],
110
- ) => React.ReactNode)
100
+ ) => ReactNode)
111
101
  }
112
102
 
113
103
  export type MakeLinkPropsOptions<
114
- TAllRouteInfo extends AnyAllRouteInfo,
115
- TFrom extends ValidFromPath<TAllRouteInfo>,
116
- TTo extends string,
117
- > = LinkPropsOptions<TAllRouteInfo, TFrom, TTo> &
118
- React.AnchorHTMLAttributes<HTMLAnchorElement>
104
+ TFrom extends ValidFromPath<RegisteredAllRouteInfo> = '/',
105
+ TTo extends string = '.',
106
+ > = LinkPropsOptions<TFrom, TTo> & React.AnchorHTMLAttributes<HTMLAnchorElement>
119
107
 
120
108
  export type MakeLinkOptions<
121
- TAllRouteInfo extends AnyAllRouteInfo,
122
- TFrom extends ValidFromPath<TAllRouteInfo>,
123
- TTo extends string,
124
- > = LinkPropsOptions<TAllRouteInfo, TFrom, TTo> &
109
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
110
+ TTo extends string = '.',
111
+ > = LinkPropsOptions<TFrom, TTo> &
125
112
  React.AnchorHTMLAttributes<HTMLAnchorElement> &
126
113
  Omit<React.AnchorHTMLAttributes<HTMLAnchorElement>, 'children'> & {
127
114
  // 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
128
- children?:
129
- | React.ReactNode
130
- | ((state: { isActive: boolean }) => React.ReactNode)
115
+ children?: ReactNode | ((state: { isActive: boolean }) => ReactNode)
131
116
  }
132
117
 
133
118
  declare module '@tanstack/router-core' {
@@ -140,84 +125,160 @@ declare module '@tanstack/router-core' {
140
125
  }
141
126
 
142
127
  interface RouterOptions<TRouteConfig, TRouterContext> {
143
- // ssrFooter?: () => JSX.Element | React.ReactNode
144
- }
145
-
146
- interface Router<
147
- TRouteConfig extends AnyRouteConfig = RouteConfig,
148
- TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
149
- > {
150
- useState: () => RouterState
151
- useRoute: <TId extends keyof TAllRouteInfo['routeInfoById']>(
152
- routeId: TId,
153
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
154
- useMatch: <
155
- TId extends keyof TAllRouteInfo['routeInfoById'],
156
- TStrict extends boolean = true,
157
- >(
158
- routeId: TId,
159
- opts?: { strict?: TStrict },
160
- ) => TStrict extends true
161
- ? RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
162
- :
163
- | RouteMatch<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TId]>
164
- | undefined
165
- linkProps: <TTo extends string = '.'>(
166
- props: MakeLinkPropsOptions<TAllRouteInfo, '/', TTo>,
167
- ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
168
- Link: <TTo extends string = '.'>(
169
- props: MakeLinkOptions<TAllRouteInfo, '/', TTo>,
170
- ) => JSX.Element
171
- MatchRoute: <TTo extends string = '.'>(
172
- props: MakeMatchRouteOptions<TAllRouteInfo, '/', TTo>,
173
- ) => JSX.Element
174
- }
175
-
176
- interface Route<
177
- TAllRouteInfo extends AnyAllRouteInfo = DefaultAllRouteInfo,
178
- TRouteInfo extends AnyRouteInfo = RouteInfo,
179
- > {
180
- useRoute: <
181
- TTo extends string = '.',
182
- TResolved extends string = ResolveRelativePath<
183
- TRouteInfo['id'],
184
- NoInfer<TTo>
185
- >,
186
- >(
187
- routeId: CheckId<
188
- TAllRouteInfo,
189
- TResolved,
190
- ToIdOption<TAllRouteInfo, TRouteInfo['id'], TTo>
191
- >,
192
- opts?: { strict?: boolean },
193
- ) => Route<TAllRouteInfo, TAllRouteInfo['routeInfoById'][TResolved]>
194
- linkProps: <TTo extends string = '.'>(
195
- props: MakeLinkPropsOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo>,
196
- ) => React.AnchorHTMLAttributes<HTMLAnchorElement>
197
- Link: <TTo extends string = '.'>(
198
- props: MakeLinkOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo>,
199
- ) => JSX.Element
200
- MatchRoute: <TTo extends string = '.'>(
201
- props: MakeMatchRouteOptions<TAllRouteInfo, TRouteInfo['fullPath'], TTo>,
202
- ) => JSX.Element
128
+ // ssrFooter?: () => JSX.Element | Node
203
129
  }
204
130
  }
205
131
 
206
132
  export type PromptProps = {
207
133
  message: string
208
134
  when?: boolean | any
209
- children?: React.ReactNode
135
+ children?: ReactNode
210
136
  }
211
137
 
212
138
  //
213
139
 
214
- export function Link<TTo extends string = '.'>(
215
- props: MakeLinkOptions<RegisteredAllRouteInfo, '/', TTo>,
216
- ): JSX.Element {
140
+ export function useLinkProps<
141
+ TFrom extends ValidFromPath<RegisteredAllRouteInfo> = '/',
142
+ TTo extends string = '.',
143
+ >(
144
+ options: MakeLinkPropsOptions<TFrom, TTo>,
145
+ ): React.AnchorHTMLAttributes<HTMLAnchorElement> {
217
146
  const router = useRouter()
218
- return <router.Link {...(props as any)} />
147
+
148
+ const {
149
+ // custom props
150
+ type,
151
+ children,
152
+ target,
153
+ activeProps = () => ({ className: 'active' }),
154
+ inactiveProps = () => ({}),
155
+ activeOptions,
156
+ disabled,
157
+ // fromCurrent,
158
+ hash,
159
+ search,
160
+ params,
161
+ to,
162
+ preload,
163
+ preloadDelay,
164
+ preloadMaxAge,
165
+ replace,
166
+ // element props
167
+ style,
168
+ className,
169
+ onClick,
170
+ onFocus,
171
+ onMouseEnter,
172
+ onMouseLeave,
173
+ onTouchStart,
174
+ onTouchEnd,
175
+ ...rest
176
+ } = options
177
+
178
+ const linkInfo = router.buildLink(options as any)
179
+
180
+ if (linkInfo.type === 'external') {
181
+ const { href } = linkInfo
182
+ return { href }
183
+ }
184
+
185
+ const { handleClick, handleFocus, handleEnter, handleLeave, isActive, next } =
186
+ linkInfo
187
+
188
+ const reactHandleClick = (e: Event) => {
189
+ if (React.startTransition) {
190
+ // This is a hack for react < 18
191
+ React.startTransition(() => {
192
+ handleClick(e)
193
+ })
194
+ } else {
195
+ handleClick(e)
196
+ }
197
+ }
198
+
199
+ const composeHandlers =
200
+ (handlers: (undefined | ((e: any) => void))[]) =>
201
+ (e: React.SyntheticEvent) => {
202
+ if (e.persist) e.persist()
203
+ handlers.filter(Boolean).forEach((handler) => {
204
+ if (e.defaultPrevented) return
205
+ handler!(e)
206
+ })
207
+ }
208
+
209
+ // Get the active props
210
+ const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> = isActive
211
+ ? functionalUpdate(activeProps, {}) ?? {}
212
+ : {}
213
+
214
+ // Get the inactive props
215
+ const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
216
+ isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
217
+
218
+ return {
219
+ ...resolvedActiveProps,
220
+ ...resolvedInactiveProps,
221
+ ...rest,
222
+ href: disabled ? undefined : next.href,
223
+ onClick: composeHandlers([onClick, reactHandleClick]),
224
+ onFocus: composeHandlers([onFocus, handleFocus]),
225
+ onMouseEnter: composeHandlers([onMouseEnter, handleEnter]),
226
+ onMouseLeave: composeHandlers([onMouseLeave, handleLeave]),
227
+ target,
228
+ style: {
229
+ ...style,
230
+ ...resolvedActiveProps.style,
231
+ ...resolvedInactiveProps.style,
232
+ },
233
+ className:
234
+ [
235
+ className,
236
+ resolvedActiveProps.className,
237
+ resolvedInactiveProps.className,
238
+ ]
239
+ .filter(Boolean)
240
+ .join(' ') || undefined,
241
+ ...(disabled
242
+ ? {
243
+ role: 'link',
244
+ 'aria-disabled': true,
245
+ }
246
+ : undefined),
247
+ ['data-status']: isActive ? 'active' : undefined,
248
+ }
249
+ }
250
+
251
+ export interface LinkFn<
252
+ TDefaultFrom extends RegisteredAllRouteInfo['routePaths'] = '/',
253
+ TDefaultTo extends string = '.',
254
+ > {
255
+ <
256
+ TFrom extends RegisteredAllRouteInfo['routePaths'] = TDefaultFrom,
257
+ TTo extends string = TDefaultTo,
258
+ >(
259
+ props: MakeLinkOptions<TFrom, TTo>,
260
+ ): ReactNode
219
261
  }
220
262
 
263
+ export const Link: LinkFn = React.forwardRef((props: any, ref) => {
264
+ const linkProps = useLinkProps(props)
265
+
266
+ return (
267
+ <a
268
+ {...{
269
+ ref: ref as any,
270
+ ...linkProps,
271
+ children:
272
+ typeof props.children === 'function'
273
+ ? props.children({
274
+ isActive: (linkProps as any)['data-status'] === 'active',
275
+ })
276
+ : props.children,
277
+ }}
278
+ />
279
+ )
280
+ }) as any
281
+
221
282
  type MatchesContextValue = RouteMatch[]
222
283
 
223
284
  export const matchesContext = React.createContext<MatchesContextValue>(null!)
@@ -227,246 +288,85 @@ export const routerContext = React.createContext<{ router: RegisteredRouter }>(
227
288
 
228
289
  export type MatchesProviderProps = {
229
290
  value: MatchesContextValue
230
- children: React.ReactNode
291
+ children: ReactNode
231
292
  }
232
293
 
233
- export function MatchesProvider(props: MatchesProviderProps) {
234
- return <matchesContext.Provider {...props} />
235
- }
294
+ const EMPTY = {}
236
295
 
237
- const useRouterSubscription = (router: Router<any, any, any>) => {
238
- useSyncExternalStore(
239
- (cb) => router.subscribe(() => cb()),
240
- () => router.state,
241
- () => router.state,
242
- )
243
- }
296
+ export const __useStoreValue = <TSeed, TReturn>(
297
+ seed: () => TSeed,
298
+ selector?: (seed: TSeed) => TReturn,
299
+ ): TReturn => {
300
+ const valueRef = React.useRef<TReturn>(EMPTY as any)
244
301
 
245
- export function createReactRouter<
246
- TRouteConfig extends AnyRouteConfig = RouteConfig,
247
- TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
248
- TRouterContext = unknown,
249
- >(
250
- opts: RouterOptions<TRouteConfig, TRouterContext>,
251
- ): Router<TRouteConfig, TAllRouteInfo, TRouterContext> {
252
- const makeRouteExt = (
253
- route: AnyRoute,
254
- router: Router<any, any, any>,
255
- ): Pick<AnyRoute, 'useRoute' | 'linkProps' | 'Link' | 'MatchRoute'> => {
256
- return {
257
- useRoute: (subRouteId = '.' as any) => {
258
- const resolvedRouteId = router.resolvePath(
259
- route.routeId,
260
- subRouteId as string,
261
- )
262
- const resolvedRoute = router.getRoute(resolvedRouteId)
263
- useRouterSubscription(router)
264
- invariant(
265
- resolvedRoute,
266
- `Could not find a route for route "${
267
- resolvedRouteId as string
268
- }"! Did you forget to add it to your route config?`,
269
- )
270
- return resolvedRoute
271
- },
272
- linkProps: (options) => {
273
- const {
274
- // custom props
275
- type,
276
- children,
277
- target,
278
- activeProps = () => ({ className: 'active' }),
279
- inactiveProps = () => ({}),
280
- activeOptions,
281
- disabled,
282
- // fromCurrent,
283
- hash,
284
- search,
285
- params,
286
- to,
287
- preload,
288
- preloadDelay,
289
- preloadMaxAge,
290
- replace,
291
- // element props
292
- style,
293
- className,
294
- onClick,
295
- onFocus,
296
- onMouseEnter,
297
- onMouseLeave,
298
- onTouchStart,
299
- onTouchEnd,
300
- ...rest
301
- } = options
302
-
303
- const linkInfo = route.buildLink(options as any)
304
-
305
- if (linkInfo.type === 'external') {
306
- const { href } = linkInfo
307
- return { href }
308
- }
302
+ // If there is no selector, track the seed
303
+ // If there is a selector, do not track the seed
304
+ const getValue = () =>
305
+ (!selector ? seed() : selector(untrack(() => seed()))) as TReturn
309
306
 
310
- const {
311
- handleClick,
312
- handleFocus,
313
- handleEnter,
314
- handleLeave,
315
- isActive,
316
- next,
317
- } = linkInfo
318
-
319
- const reactHandleClick = (e: Event) => {
320
- if (React.startTransition)
321
- // This is a hack for react < 18
322
- React.startTransition(() => {
323
- handleClick(e)
324
- })
325
- else handleClick(e)
326
- }
327
-
328
- const composeHandlers =
329
- (handlers: (undefined | ((e: any) => void))[]) =>
330
- (e: React.SyntheticEvent) => {
331
- if (e.persist) e.persist()
332
- handlers.forEach((handler) => {
333
- if (e.defaultPrevented) return
334
- if (handler) handler(e)
335
- })
336
- }
307
+ // If empty, initialize the value
308
+ if (valueRef.current === EMPTY) {
309
+ valueRef.current = sharedClone(undefined, getValue())
310
+ }
337
311
 
338
- // Get the active props
339
- const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> =
340
- isActive ? functionalUpdate(activeProps, {}) ?? {} : {}
341
-
342
- // Get the inactive props
343
- const resolvedInactiveProps: React.HTMLAttributes<HTMLAnchorElement> =
344
- isActive ? {} : functionalUpdate(inactiveProps, {}) ?? {}
345
-
346
- return {
347
- ...resolvedActiveProps,
348
- ...resolvedInactiveProps,
349
- ...rest,
350
- href: disabled ? undefined : next.href,
351
- onClick: composeHandlers([reactHandleClick, onClick]),
352
- onFocus: composeHandlers([handleFocus, onFocus]),
353
- onMouseEnter: composeHandlers([handleEnter, onMouseEnter]),
354
- onMouseLeave: composeHandlers([handleLeave, onMouseLeave]),
355
- target,
356
- style: {
357
- ...style,
358
- ...resolvedActiveProps.style,
359
- ...resolvedInactiveProps.style,
360
- },
361
- className:
362
- [
363
- className,
364
- resolvedActiveProps.className,
365
- resolvedInactiveProps.className,
366
- ]
367
- .filter(Boolean)
368
- .join(' ') || undefined,
369
- ...(disabled
370
- ? {
371
- role: 'link',
372
- 'aria-disabled': true,
373
- }
374
- : undefined),
375
- ['data-status']: isActive ? 'active' : undefined,
376
- }
377
- },
378
- Link: React.forwardRef((props: any, ref) => {
379
- const linkProps = route.linkProps(props)
380
-
381
- useRouterSubscription(router)
382
-
383
- return (
384
- <a
385
- {...{
386
- ref: ref as any,
387
- ...linkProps,
388
- children:
389
- typeof props.children === 'function'
390
- ? props.children({
391
- isActive: (linkProps as any)['data-status'] === 'active',
392
- })
393
- : props.children,
394
- }}
395
- />
312
+ // Snapshot should just return the current cached value
313
+ const getSnapshot = React.useCallback(() => valueRef.current, [])
314
+
315
+ const getStore = React.useCallback((cb: () => void) => {
316
+ // A root is necessary to track effects
317
+ return createRoot(() => {
318
+ createEffect(() => {
319
+ // Read and update the value
320
+ // getValue will handle which values are accessed and
321
+ // thus tracked.
322
+ // sharedClone will both recursively track the end result
323
+ // and ensure that the previous value is structurally shared
324
+ // into the new version.
325
+ valueRef.current = unwrap(
326
+ // Unwrap the value to get rid of any proxy structures
327
+ sharedClone(valueRef.current, getValue()),
396
328
  )
397
- }) as any,
398
- MatchRoute: (opts) => {
399
- const { pending, caseSensitive, children, ...rest } = opts
400
-
401
- const params = route.matchRoute(rest as any, {
402
- pending,
403
- caseSensitive,
404
- })
329
+ cb()
330
+ })
331
+ })
332
+ }, [])
405
333
 
406
- if (!params) {
407
- return null
408
- }
334
+ return useSyncExternalStore(getStore, getSnapshot, getSnapshot)
335
+ }
409
336
 
410
- return typeof opts.children === 'function'
411
- ? opts.children(params as any)
412
- : (opts.children as any)
413
- },
414
- }
415
- }
337
+ const [store, setStore] = createStore({ foo: 'foo', bar: { baz: 'baz' } })
416
338
 
417
- const coreRouter = createRouter<TRouteConfig>({
418
- ...opts,
419
- createRouter: (router) => {
420
- const routerExt: Pick<Router<any, any, any>, 'useMatch' | 'useState'> = {
421
- useState: () => {
422
- useRouterSubscription(router)
423
- return router.state
424
- },
425
- useMatch: (routeId, opts) => {
426
- useRouterSubscription(router)
427
-
428
- const nearestMatch = useNearestMatch()
429
- const match = router.state.currentMatches.find(
430
- (d) => d.routeId === routeId,
431
- )
432
-
433
- if (opts?.strict ?? true) {
434
- invariant(
435
- match,
436
- `Could not find an active match for "${routeId as string}"!`,
437
- )
438
-
439
- invariant(
440
- nearestMatch.routeId == match?.routeId,
441
- `useMatch("${
442
- match?.routeId as string
443
- }") is being called in a component that is meant to render the '${
444
- nearestMatch.routeId
445
- }' route. Did you mean to 'useMatch("${
446
- match?.routeId as string
447
- }", { strict: false })' or 'useRoute("${
448
- match?.routeId as string
449
- }")' instead?`,
450
- )
451
- }
339
+ createRoot(() => {
340
+ let prev: any
452
341
 
453
- return match as any
454
- },
455
- }
342
+ createEffect(() => {
343
+ console.log('effect')
344
+ const next = sharedClone(prev, store)
345
+ console.log(next)
346
+ prev = untrack(() => next)
347
+ })
348
+ })
456
349
 
457
- const routeExt = makeRouteExt(router.getRoute(rootRouteId), router)
350
+ setStore((s) => {
351
+ s.foo = '1'
352
+ })
458
353
 
459
- Object.assign(router, routerExt, routeExt)
460
- },
461
- createRoute: ({ router, route }) => {
462
- const routeExt = makeRouteExt(route, router)
354
+ setStore((s) => {
355
+ s.bar.baz = '2'
356
+ })
463
357
 
464
- Object.assign(route, routeExt)
465
- },
358
+ export function createReactRouter<
359
+ TRouteConfig extends AnyRouteConfig = RouteConfig,
360
+ TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
361
+ TRouterContext = unknown,
362
+ >(
363
+ opts: RouterOptions<TRouteConfig, TRouterContext>,
364
+ ): Router<TRouteConfig, TAllRouteInfo, TRouterContext> {
365
+ const coreRouter = createRouter<TRouteConfig>({
366
+ ...opts,
466
367
  loadComponent: async (component) => {
467
- if (component.preload && typeof document !== 'undefined') {
468
- component.preload()
469
- // return await component.preload()
368
+ if (component.preload) {
369
+ await component.preload()
470
370
  }
471
371
 
472
372
  return component as any
@@ -494,17 +394,21 @@ export function RouterProvider<
494
394
  }: RouterProps<TRouteConfig, TAllRouteInfo, TRouterContext>) {
495
395
  router.update(rest)
496
396
 
497
- useRouterSubscription(router)
498
- React.useEffect(() => {
499
- return router.mount()
500
- }, [router])
397
+ const [, , currentMatches] = __useStoreValue(
398
+ () => router.store,
399
+ (s) => [s.status, s.pendingMatches, s.currentMatches],
400
+ )
401
+
402
+ React.useEffect(router.mount, [router])
403
+
404
+ console.log('current', currentMatches)
501
405
 
502
406
  return (
503
407
  <>
504
408
  <routerContext.Provider value={{ router: router as any }}>
505
- <MatchesProvider value={[undefined!, ...router.state.currentMatches]}>
409
+ <matchesContext.Provider value={[undefined!, ...currentMatches]}>
506
410
  <Outlet />
507
- </MatchesProvider>
411
+ </matchesContext.Provider>
508
412
  </routerContext.Provider>
509
413
  </>
510
414
  )
@@ -513,77 +417,191 @@ export function RouterProvider<
513
417
  export function useRouter(): RegisteredRouter {
514
418
  const value = React.useContext(routerContext)
515
419
  warning(!value, 'useRouter must be used inside a <Router> component!')
516
-
517
- useRouterSubscription(value.router)
518
-
519
420
  return value.router
520
421
  }
521
422
 
423
+ export function useRouterStore<T = RouterStore>(
424
+ selector?: (state: Router['store']) => T,
425
+ ): T {
426
+ const router = useRouter()
427
+ return __useStoreValue(() => router.store, selector)
428
+ }
429
+
522
430
  export function useMatches(): RouteMatch[] {
523
431
  return React.useContext(matchesContext)
524
432
  }
525
433
 
526
434
  export function useMatch<
527
- TId extends keyof RegisteredAllRouteInfo['routeInfoById'],
435
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'],
528
436
  TStrict extends boolean = true,
529
- >(
530
- routeId: TId,
531
- opts?: { strict?: TStrict },
532
- ): TStrict extends true
533
- ? RouteMatch<
534
- RegisteredAllRouteInfo,
535
- RegisteredAllRouteInfo['routeInfoById'][TId]
536
- >
537
- :
538
- | RouteMatch<
539
- RegisteredAllRouteInfo,
540
- RegisteredAllRouteInfo['routeInfoById'][TId]
541
- >
542
- | undefined {
437
+ TRouteMatch = RouteMatch<
438
+ RegisteredAllRouteInfo,
439
+ RegisteredAllRouteInfo['routeInfoById'][TFrom]
440
+ >,
441
+ // TSelected = TRouteMatch,
442
+ >(opts?: {
443
+ from: TFrom
444
+ strict?: TStrict
445
+ // select?: (match: TRouteMatch) => TSelected
446
+ }): TStrict extends true ? TRouteMatch : TRouteMatch | undefined {
543
447
  const router = useRouter()
544
- return router.useMatch(routeId as any, opts) as any
545
- }
448
+ const nearestMatch = useMatches()[0]!
449
+ const match = opts?.from
450
+ ? router.store.currentMatches.find((d) => d.routeId === opts?.from)
451
+ : nearestMatch
452
+
453
+ invariant(
454
+ match,
455
+ `Could not find ${
456
+ opts?.from ? `an active match from "${opts.from}"` : 'a nearest match!'
457
+ }`,
458
+ )
546
459
 
547
- export function useNearestMatch(): RouteMatch<
548
- RegisteredAllRouteInfo,
549
- RouteInfo
550
- > {
551
- const runtimeMatch = useMatches()[0]
460
+ if (opts?.strict ?? true) {
461
+ invariant(
462
+ nearestMatch.routeId == match?.routeId,
463
+ `useMatch("${
464
+ match?.routeId as string
465
+ }") is being called in a component that is meant to render the '${
466
+ nearestMatch.routeId
467
+ }' route. Did you mean to 'useMatch("${
468
+ match?.routeId as string
469
+ }", { strict: false })' or 'useRoute("${
470
+ match?.routeId as string
471
+ }")' instead?`,
472
+ )
473
+ }
552
474
 
553
- invariant(runtimeMatch, `Could not find a nearest match!`)
475
+ __useStoreValue(() => match!.store)
554
476
 
555
- return runtimeMatch as any
477
+ return match as any
556
478
  }
557
479
 
558
480
  export function useRoute<
559
- TId extends keyof RegisteredAllRouteInfo['routeInfoById'],
481
+ TId extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
560
482
  >(
561
483
  routeId: TId,
562
484
  ): Route<RegisteredAllRouteInfo, RegisteredAllRouteInfo['routeInfoById'][TId]> {
563
485
  const router = useRouter()
564
- return router.useRoute(routeId as any) as any
486
+ const resolvedRoute = router.getRoute(routeId as any)
487
+
488
+ invariant(
489
+ resolvedRoute,
490
+ `Could not find a route for route "${
491
+ routeId as string
492
+ }"! Did you forget to add it to your route config?`,
493
+ )
494
+
495
+ return resolvedRoute as any
565
496
  }
566
497
 
567
- export function useSearch(): RegisteredAllRouteInfo['fullSearchSchema'] {
568
- return useRouter().state.currentLocation.search
498
+ export function useLoaderData<
499
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
500
+ TStrict extends boolean = true,
501
+ TLoaderData = RegisteredAllRouteInfo['routeInfoById'][TFrom]['loaderData'],
502
+ TSelected = TLoaderData,
503
+ >(opts?: {
504
+ from: TFrom
505
+ strict?: TStrict
506
+ select?: (loaderData: TLoaderData) => TSelected
507
+ }): TStrict extends true ? TSelected : TSelected | undefined {
508
+ const match = useMatch(opts) as any
509
+ return __useStoreValue(() => match?.store.loaderData, opts?.select)
569
510
  }
570
511
 
571
- export function useParams(): RegisteredAllRouteInfo['allParams'] {
572
- return last(useRouter().state.currentMatches)?.params as any
512
+ export function useSearch<
513
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'],
514
+ TStrict extends boolean = true,
515
+ TSearch = RegisteredAllRouteInfo['routeInfoById'][TFrom]['fullSearchSchema'],
516
+ TSelected = TSearch,
517
+ >(opts?: {
518
+ from: TFrom
519
+ strict?: TStrict
520
+ select?: (search: TSearch) => TSelected
521
+ }): TStrict extends true ? TSelected : TSelected | undefined {
522
+ const match = useMatch(opts)
523
+ return __useStoreValue(() => match?.store.search, opts?.select) as any
573
524
  }
574
525
 
575
- export function linkProps<TTo extends string = '.'>(
576
- props: MakeLinkPropsOptions<RegisteredAllRouteInfo, '/', TTo>,
577
- ): React.AnchorHTMLAttributes<HTMLAnchorElement> {
526
+ export function useParams<
527
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
528
+ TDefaultSelected = Expand<
529
+ RegisteredAllRouteInfo['allParams'] &
530
+ RegisteredAllRouteInfo['routeInfoById'][TFrom]['allParams']
531
+ >,
532
+ TSelected = TDefaultSelected,
533
+ >(opts?: {
534
+ from: TFrom
535
+ select?: (search: TDefaultSelected) => TSelected
536
+ }): TSelected {
578
537
  const router = useRouter()
579
- return router.linkProps(props as any)
538
+ return __useStoreValue(
539
+ () => last(router.store.currentMatches)?.params as any,
540
+ opts?.select,
541
+ )
542
+ }
543
+
544
+ export function useNavigate<
545
+ TDefaultFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
546
+ >(defaultOpts: { from?: TDefaultFrom }) {
547
+ return <
548
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = TDefaultFrom,
549
+ TTo extends string = '.',
550
+ >(
551
+ opts: MakeLinkOptions<TFrom, TTo>,
552
+ ) => {
553
+ const router = useRouter()
554
+ return router.navigate({ ...defaultOpts, ...(opts as any) })
555
+ }
556
+ }
557
+
558
+ export function useAction<
559
+ TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
560
+ TFromRoute extends RegisteredAllRouteInfo['routeInfoById'][TFrom] = RegisteredAllRouteInfo['routeInfoById'][TFrom],
561
+ >(opts: {
562
+ from: TFrom
563
+ }): Action<TFromRoute['actionPayload'], TFromRoute['actionResponse']> {
564
+ const route = useRoute(opts.from)
565
+ const action = route.action
566
+ __useStoreValue(() => action)
567
+ return action as any
580
568
  }
581
569
 
582
- export function MatchRoute<TTo extends string = '.'>(
583
- props: MakeMatchRouteOptions<RegisteredAllRouteInfo, '/', TTo>,
584
- ): JSX.Element {
570
+ export function useMatchRoute() {
585
571
  const router = useRouter()
586
- return React.createElement(router.MatchRoute, props as any)
572
+
573
+ return <
574
+ TFrom extends ValidFromPath<RegisteredAllRouteInfo> = '/',
575
+ TTo extends string = '.',
576
+ >(
577
+ opts: MakeUseMatchRouteOptions<TFrom, TTo>,
578
+ ) => {
579
+ const { pending, caseSensitive, ...rest } = opts
580
+
581
+ return router.matchRoute(rest as any, {
582
+ pending,
583
+ caseSensitive,
584
+ })
585
+ }
586
+ }
587
+
588
+ export function MatchRoute<
589
+ TFrom extends ValidFromPath<RegisteredAllRouteInfo> = '/',
590
+ TTo extends string = '.',
591
+ >(props: MakeMatchRouteOptions<TFrom, TTo>): any {
592
+ const matchRoute = useMatchRoute()
593
+ const params = matchRoute(props)
594
+
595
+ if (!params) {
596
+ return null
597
+ }
598
+
599
+ return React.createElement(
600
+ typeof props.children === 'function'
601
+ ? (props.children as any)(params)
602
+ : props.children,
603
+ props as any,
604
+ )
587
605
  }
588
606
 
589
607
  export function Outlet() {
@@ -593,6 +611,31 @@ export function Outlet() {
593
611
 
594
612
  const defaultPending = React.useCallback(() => null, [])
595
613
 
614
+ __useStoreValue(() => match?.store)
615
+
616
+ const Inner = React.useCallback((props: { match: RouteMatch }): any => {
617
+ if (props.match.store.status === 'error') {
618
+ throw props.match.store.error
619
+ }
620
+
621
+ if (props.match.store.status === 'success') {
622
+ return React.createElement(
623
+ (props.match.__.component as any) ??
624
+ router.options.defaultComponent ??
625
+ Outlet,
626
+ )
627
+ }
628
+
629
+ if (props.match.store.status === 'loading') {
630
+ throw props.match.__.loadPromise
631
+ }
632
+
633
+ invariant(
634
+ false,
635
+ 'Idle routeMatch status encountered during rendering! You should never see this. File an issue!',
636
+ )
637
+ }, [])
638
+
596
639
  if (!match) {
597
640
  return null
598
641
  }
@@ -605,29 +648,14 @@ export function Outlet() {
605
648
  match.__.errorComponent ?? router.options.defaultErrorComponent
606
649
 
607
650
  return (
608
- <MatchesProvider value={matches}>
651
+ <matchesContext.Provider value={matches}>
609
652
  <React.Suspense fallback={<PendingComponent />}>
610
653
  <CatchBoundary
611
654
  key={match.routeId}
612
655
  errorComponent={errorComponent}
613
656
  match={match as any}
614
657
  >
615
- {
616
- ((): React.ReactNode => {
617
- if (match.status === 'error') {
618
- throw match.error
619
- }
620
-
621
- if (match.status === 'success') {
622
- return React.createElement(
623
- (match.__.component as any) ??
624
- router.options.defaultComponent ??
625
- Outlet,
626
- )
627
- }
628
- throw match.__.loadPromise
629
- })() as JSX.Element
630
- }
658
+ <Inner match={match} />
631
659
  </CatchBoundary>
632
660
  </React.Suspense>
633
661
  {/* Provide a suffix suspense boundary to make sure the router is
@@ -635,7 +663,7 @@ export function Outlet() {
635
663
  {/* {router.options.ssrFooter && match.matchId === rootRouteId ? (
636
664
  <React.Suspense fallback={null}>
637
665
  {(() => {
638
- if (router.state.pending) {
666
+ if (router.store.pending) {
639
667
  throw router.navigationPromise
640
668
  }
641
669
 
@@ -643,7 +671,7 @@ export function Outlet() {
643
671
  })()}
644
672
  </React.Suspense>
645
673
  ) : null} */}
646
- </MatchesProvider>
674
+ </matchesContext.Provider>
647
675
  )
648
676
  }
649
677
 
@@ -695,13 +723,15 @@ function CatchBoundaryInner(props: {
695
723
 
696
724
  React.useEffect(() => {
697
725
  if (activeErrorState) {
698
- let prevKey = router.state.currentLocation.key
699
- return router.subscribe(() => {
700
- if (router.state.currentLocation.key !== prevKey) {
701
- prevKey = router.state.currentLocation.key
702
- setActiveErrorState({} as any)
703
- }
704
- })
726
+ let prevKey = router.store.currentLocation.key
727
+ return createRoot(() =>
728
+ createEffect(() => {
729
+ if (router.store.currentLocation.key !== prevKey) {
730
+ prevKey = router.store.currentLocation.key
731
+ setActiveErrorState({} as any)
732
+ }
733
+ }),
734
+ )
705
735
  }
706
736
 
707
737
  return
@@ -714,7 +744,7 @@ function CatchBoundaryInner(props: {
714
744
  props.reset()
715
745
  }, [props.errorState.error])
716
746
 
717
- if (activeErrorState.error) {
747
+ if (props.errorState.error) {
718
748
  return React.createElement(errorComponent, activeErrorState)
719
749
  }
720
750
 
@@ -758,7 +788,7 @@ export function usePrompt(message: string, when: boolean | any): void {
758
788
  unblock()
759
789
  transition.retry()
760
790
  } else {
761
- router.state.currentLocation.pathname = window.location.pathname
791
+ router.store.currentLocation.pathname = window.location.pathname
762
792
  }
763
793
  })
764
794
 
@@ -768,5 +798,22 @@ export function usePrompt(message: string, when: boolean | any): void {
768
798
 
769
799
  export function Prompt({ message, when, children }: PromptProps) {
770
800
  usePrompt(message, when ?? true)
771
- return (children ?? null) as React.ReactNode
801
+ return (children ?? null) as ReactNode
772
802
  }
803
+
804
+ // function circularStringify(obj: any) {
805
+ // const seen = new Set()
806
+
807
+ // return (
808
+ // JSON.stringify(obj, (_, value) => {
809
+ // if (typeof value === 'function') {
810
+ // return undefined
811
+ // }
812
+ // if (typeof value === 'object' && value !== null) {
813
+ // if (seen.has(value)) return
814
+ // seen.add(value)
815
+ // }
816
+ // return value
817
+ // }) || ''
818
+ // )
819
+ // }