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