@tanstack/react-router 0.0.1-beta.48 → 0.0.1-beta.49

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/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tanstack/react-router",
3
3
  "author": "Tanner Linsley",
4
- "version": "0.0.1-beta.48",
4
+ "version": "0.0.1-beta.49",
5
5
  "license": "MIT",
6
6
  "repository": "tanstack/router",
7
7
  "homepage": "https://tanstack.com/router/",
@@ -41,9 +41,9 @@
41
41
  },
42
42
  "dependencies": {
43
43
  "@babel/runtime": "^7.16.7",
44
- "@solidjs/reactivity": "^0.0.6",
44
+ "@solidjs/reactivity": "^0.0.7",
45
45
  "use-sync-external-store": "^1.2.0",
46
- "@tanstack/router-core": "0.0.1-beta.45"
46
+ "@tanstack/router-core": "0.0.1-beta.49"
47
47
  },
48
48
  "devDependencies": {
49
49
  "@types/use-sync-external-store": "^0.0.3",
package/src/index.tsx CHANGED
@@ -1,19 +1,11 @@
1
1
  import * as React from 'react'
2
2
 
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'
8
-
9
3
  import {
10
4
  Route,
11
5
  RegisteredAllRouteInfo,
12
6
  RegisteredRouter,
13
7
  RouterStore,
14
8
  last,
15
- sharedClone,
16
- Action,
17
9
  warning,
18
10
  RouterOptions,
19
11
  RouteMatch,
@@ -23,7 +15,6 @@ import {
23
15
  AnyAllRouteInfo,
24
16
  DefaultAllRouteInfo,
25
17
  functionalUpdate,
26
- createRouter,
27
18
  AllRouteInfo,
28
19
  ValidFromPath,
29
20
  LinkOptions,
@@ -34,11 +25,19 @@ import {
34
25
  invariant,
35
26
  Router,
36
27
  Expand,
28
+ Action,
29
+ ActionStore,
30
+ ActionSubmission,
37
31
  } from '@tanstack/router-core'
32
+ import { useStore } from './useStore'
33
+
34
+ //
38
35
 
39
36
  export * from '@tanstack/router-core'
40
37
 
41
- export * from '@solidjs/reactivity'
38
+ export { useStore }
39
+
40
+ //
42
41
 
43
42
  type ReactNode = any
44
43
 
@@ -158,7 +157,7 @@ export function useLinkProps<
158
157
  hash,
159
158
  search,
160
159
  params,
161
- to,
160
+ to = '.',
162
161
  preload,
163
162
  preloadDelay,
164
163
  preloadMaxAge,
@@ -208,7 +207,7 @@ export function useLinkProps<
208
207
 
209
208
  // Get the active props
210
209
  const resolvedActiveProps: React.HTMLAttributes<HTMLAnchorElement> = isActive
211
- ? functionalUpdate(activeProps, {}) ?? {}
210
+ ? functionalUpdate(activeProps as any, {}) ?? {}
212
211
  : {}
213
212
 
214
213
  // Get the inactive props
@@ -291,89 +290,23 @@ export type MatchesProviderProps = {
291
290
  children: ReactNode
292
291
  }
293
292
 
294
- const EMPTY = {}
295
-
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)
301
-
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
306
-
307
- // If empty, initialize the value
308
- if (valueRef.current === EMPTY) {
309
- valueRef.current = sharedClone(undefined, getValue())
310
- }
311
-
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()),
328
- )
329
- cb()
330
- })
331
- })
332
- }, [])
333
-
334
- return useSyncExternalStore(getStore, getSnapshot, getSnapshot)
335
- }
336
-
337
- const [store, setStore] = createStore({ foo: 'foo', bar: { baz: 'baz' } })
338
-
339
- createRoot(() => {
340
- let prev: any
341
-
342
- createEffect(() => {
343
- console.log('effect')
344
- const next = sharedClone(prev, store)
345
- console.log(next)
346
- prev = untrack(() => next)
347
- })
348
- })
349
-
350
- setStore((s) => {
351
- s.foo = '1'
352
- })
353
-
354
- setStore((s) => {
355
- s.bar.baz = '2'
356
- })
357
-
358
- export function createReactRouter<
293
+ export class ReactRouter<
359
294
  TRouteConfig extends AnyRouteConfig = RouteConfig,
360
295
  TAllRouteInfo extends AnyAllRouteInfo = AllRouteInfo<TRouteConfig>,
361
296
  TRouterContext = unknown,
362
- >(
363
- opts: RouterOptions<TRouteConfig, TRouterContext>,
364
- ): Router<TRouteConfig, TAllRouteInfo, TRouterContext> {
365
- const coreRouter = createRouter<TRouteConfig>({
366
- ...opts,
367
- loadComponent: async (component) => {
368
- if (component.preload) {
369
- await component.preload()
370
- }
371
-
372
- return component as any
373
- },
374
- })
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()
304
+ }
375
305
 
376
- return coreRouter as any
306
+ return component as any
307
+ },
308
+ })
309
+ }
377
310
  }
378
311
 
379
312
  export type RouterProps<
@@ -394,15 +327,14 @@ export function RouterProvider<
394
327
  }: RouterProps<TRouteConfig, TAllRouteInfo, TRouterContext>) {
395
328
  router.update(rest)
396
329
 
397
- const [, , currentMatches] = __useStoreValue(
398
- () => router.store,
330
+ const [, , currentMatches] = useStore(
331
+ router.store,
399
332
  (s) => [s.status, s.pendingMatches, s.currentMatches],
333
+ true,
400
334
  )
401
335
 
402
336
  React.useEffect(router.mount, [router])
403
337
 
404
- console.log('current', currentMatches)
405
-
406
338
  return (
407
339
  <>
408
340
  <routerContext.Provider value={{ router: router as any }}>
@@ -422,9 +354,10 @@ export function useRouter(): RegisteredRouter {
422
354
 
423
355
  export function useRouterStore<T = RouterStore>(
424
356
  selector?: (state: Router['store']) => T,
357
+ shallow?: boolean,
425
358
  ): T {
426
359
  const router = useRouter()
427
- return __useStoreValue(() => router.store, selector)
360
+ return useStore(router.store, selector as any, shallow)
428
361
  }
429
362
 
430
363
  export function useMatches(): RouteMatch[] {
@@ -438,16 +371,16 @@ export function useMatch<
438
371
  RegisteredAllRouteInfo,
439
372
  RegisteredAllRouteInfo['routeInfoById'][TFrom]
440
373
  >,
441
- // TSelected = TRouteMatch,
442
374
  >(opts?: {
443
375
  from: TFrom
444
376
  strict?: TStrict
445
- // select?: (match: TRouteMatch) => TSelected
377
+ track?: (match: TRouteMatch) => any
378
+ shallow?: boolean
446
379
  }): TStrict extends true ? TRouteMatch : TRouteMatch | undefined {
447
380
  const router = useRouter()
448
381
  const nearestMatch = useMatches()[0]!
449
382
  const match = opts?.from
450
- ? router.store.currentMatches.find((d) => d.routeId === opts?.from)
383
+ ? router.store.state.currentMatches.find((d) => d.route.id === opts?.from)
451
384
  : nearestMatch
452
385
 
453
386
  invariant(
@@ -459,20 +392,24 @@ export function useMatch<
459
392
 
460
393
  if (opts?.strict ?? true) {
461
394
  invariant(
462
- nearestMatch.routeId == match?.routeId,
395
+ nearestMatch.route.id == match?.route.id,
463
396
  `useMatch("${
464
- match?.routeId as string
397
+ match?.route.id as string
465
398
  }") is being called in a component that is meant to render the '${
466
- nearestMatch.routeId
399
+ nearestMatch.route.id
467
400
  }' route. Did you mean to 'useMatch("${
468
- match?.routeId as string
401
+ match?.route.id as string
469
402
  }", { strict: false })' or 'useRoute("${
470
- match?.routeId as string
403
+ match?.route.id as string
471
404
  }")' instead?`,
472
405
  )
473
406
  }
474
407
 
475
- __useStoreValue(() => match!.store)
408
+ useStore(
409
+ match!.store,
410
+ (d) => opts?.track?.(match as any) ?? match,
411
+ opts?.shallow,
412
+ )
476
413
 
477
414
  return match as any
478
415
  }
@@ -499,14 +436,26 @@ export function useLoaderData<
499
436
  TFrom extends keyof RegisteredAllRouteInfo['routeInfoById'] = '/',
500
437
  TStrict extends boolean = true,
501
438
  TLoaderData = RegisteredAllRouteInfo['routeInfoById'][TFrom]['loaderData'],
502
- TSelected = TLoaderData,
503
439
  >(opts?: {
504
440
  from: TFrom
505
441
  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)
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
+ )
452
+
453
+ useStore(
454
+ (match as any).store,
455
+ (d: any) => opts?.track?.(d.loaderData) ?? d.loaderData,
456
+ )
457
+
458
+ return (match as unknown as RouteMatch).store.state.loaderData as any
510
459
  }
511
460
 
512
461
  export function useSearch<
@@ -517,10 +466,15 @@ export function useSearch<
517
466
  >(opts?: {
518
467
  from: TFrom
519
468
  strict?: TStrict
520
- select?: (search: TSearch) => TSelected
469
+ track?: (search: TSearch) => TSelected
521
470
  }): TStrict extends true ? TSelected : TSelected | undefined {
522
471
  const match = useMatch(opts)
523
- return __useStoreValue(() => match?.store.search, opts?.select) as any
472
+ useStore(
473
+ (match as any).store,
474
+ (d: any) => opts?.track?.(d.search) ?? d.search,
475
+ )
476
+
477
+ return (match as unknown as RouteMatch).store.state.search as any
524
478
  }
525
479
 
526
480
  export function useParams<
@@ -532,13 +486,15 @@ export function useParams<
532
486
  TSelected = TDefaultSelected,
533
487
  >(opts?: {
534
488
  from: TFrom
535
- select?: (search: TDefaultSelected) => TSelected
489
+ track?: (search: TDefaultSelected) => TSelected
536
490
  }): TSelected {
537
491
  const router = useRouter()
538
- return __useStoreValue(
539
- () => last(router.store.currentMatches)?.params as any,
540
- opts?.select,
541
- )
492
+ useStore(router.store, (d) => {
493
+ const params = last(d.currentMatches)?.params as any
494
+ return opts?.track?.(params) ?? params
495
+ })
496
+
497
+ return last(router.store.state.currentMatches)?.params as any
542
498
  }
543
499
 
544
500
  export function useNavigate<
@@ -555,18 +511,6 @@ export function useNavigate<
555
511
  }
556
512
  }
557
513
 
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
568
- }
569
-
570
514
  export function useMatchRoute() {
571
515
  const router = useRouter()
572
516
 
@@ -596,38 +540,51 @@ export function MatchRoute<
596
540
  return null
597
541
  }
598
542
 
599
- return React.createElement(
600
- typeof props.children === 'function'
601
- ? (props.children as any)(params)
602
- : props.children,
603
- props as any,
604
- )
543
+ if (typeof props.children === 'function') {
544
+ return (props.children as any)(params)
545
+ }
546
+
547
+ return params ? props.children : null
605
548
  }
606
549
 
607
550
  export function Outlet() {
608
- const router = useRouter()
609
551
  const matches = useMatches().slice(1)
610
552
  const match = matches[0]
611
553
 
612
- const defaultPending = React.useCallback(() => null, [])
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)
613
570
 
614
- __useStoreValue(() => match?.store)
571
+ const defaultPending = React.useCallback(() => null, [])
615
572
 
616
573
  const Inner = React.useCallback((props: { match: RouteMatch }): any => {
617
- if (props.match.store.status === 'error') {
618
- throw props.match.store.error
574
+ if (props.match.store.state.status === 'error') {
575
+ throw props.match.store.state.error
619
576
  }
620
577
 
621
- if (props.match.store.status === 'success') {
578
+ if (props.match.store.state.status === 'success') {
622
579
  return React.createElement(
623
- (props.match.__.component as any) ??
580
+ (props.match.component as any) ??
624
581
  router.options.defaultComponent ??
625
582
  Outlet,
626
583
  )
627
584
  }
628
585
 
629
- if (props.match.store.status === 'loading') {
630
- throw props.match.__.loadPromise
586
+ if (props.match.store.state.status === 'loading') {
587
+ throw props.match.__loadPromise
631
588
  }
632
589
 
633
590
  invariant(
@@ -636,22 +593,18 @@ export function Outlet() {
636
593
  )
637
594
  }, [])
638
595
 
639
- if (!match) {
640
- return null
641
- }
642
-
643
- const PendingComponent = (match.__.pendingComponent ??
596
+ const PendingComponent = (match.pendingComponent ??
644
597
  router.options.defaultPendingComponent ??
645
598
  defaultPending) as any
646
599
 
647
600
  const errorComponent =
648
- match.__.errorComponent ?? router.options.defaultErrorComponent
601
+ match.errorComponent ?? router.options.defaultErrorComponent
649
602
 
650
603
  return (
651
604
  <matchesContext.Provider value={matches}>
652
605
  <React.Suspense fallback={<PendingComponent />}>
653
606
  <CatchBoundary
654
- key={match.routeId}
607
+ key={match.route.id}
655
608
  errorComponent={errorComponent}
656
609
  match={match as any}
657
610
  >
@@ -686,7 +639,7 @@ class CatchBoundary extends React.Component<{
686
639
  }
687
640
 
688
641
  componentDidCatch(error: any, info: any) {
689
- console.error(`Error in route match: ${this.props.match.matchId}`)
642
+ console.error(`Error in route match: ${this.props.match.id}`)
690
643
  console.error(error)
691
644
 
692
645
  this.setState({
@@ -721,21 +674,23 @@ function CatchBoundaryInner(props: {
721
674
  const router = useRouter()
722
675
  const errorComponent = props.errorComponent ?? DefaultErrorBoundary
723
676
 
724
- React.useEffect(() => {
725
- if (activeErrorState) {
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
- )
735
- }
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
+ // })
736
687
 
737
- return
738
- }, [activeErrorState])
688
+ // return dispose
689
+ // })
690
+ // }
691
+
692
+ // return
693
+ // }, [activeErrorState])
739
694
 
740
695
  React.useEffect(() => {
741
696
  if (props.errorState.error) {
@@ -777,43 +732,61 @@ export function DefaultErrorBoundary({ error }: { error: any }) {
777
732
  )
778
733
  }
779
734
 
780
- export function usePrompt(message: string, when: boolean | any): void {
781
- const router = useRouter()
782
-
783
- React.useEffect(() => {
784
- if (!when) return
785
-
786
- let unblock = router.history.block((transition) => {
787
- if (window.confirm(message)) {
788
- unblock()
789
- transition.retry()
790
- } else {
791
- router.store.currentLocation.pathname = window.location.pathname
792
- }
793
- })
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
+ })
794
763
 
795
- return unblock
796
- }, [when, message])
764
+ return ref as any
797
765
  }
798
766
 
799
- export function Prompt({ message, when, children }: PromptProps) {
800
- usePrompt(message, when ?? true)
801
- return (children ?? null) as ReactNode
802
- }
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
+ // })
803
784
 
804
- // function circularStringify(obj: any) {
805
- // const seen = new Set()
785
+ // return unblock
786
+ // }, [when, message])
787
+ // }
806
788
 
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
- // )
789
+ // export function Prompt({ message, when, children }: PromptProps) {
790
+ // usePrompt(message, when ?? true)
791
+ // return (children ?? null) as ReactNode
819
792
  // }
@@ -0,0 +1,70 @@
1
+ import { Store } from '@tanstack/router-core'
2
+ import { useSyncExternalStoreWithSelector } from 'use-sync-external-store/shim/with-selector'
3
+
4
+ export function useStore<TState, TSelected = TState>(
5
+ store: Store<TState>,
6
+ selector: (state: TState) => TSelected = (d) => d as any,
7
+ compareShallow?: boolean,
8
+ ) {
9
+ const slice = useSyncExternalStoreWithSelector(
10
+ store.subscribe,
11
+ () => store.state,
12
+ () => store.state,
13
+ selector,
14
+ compareShallow ? shallow : undefined,
15
+ )
16
+
17
+ return slice
18
+ }
19
+
20
+ function shallow<T>(objA: T, objB: T) {
21
+ if (Object.is(objA, objB)) {
22
+ return true
23
+ }
24
+
25
+ if (
26
+ typeof objA !== 'object' ||
27
+ objA === null ||
28
+ typeof objB !== 'object' ||
29
+ objB === null
30
+ ) {
31
+ return false
32
+ }
33
+
34
+ // if (objA instanceof Map && objB instanceof Map) {
35
+ // if (objA.size !== objB.size) return false
36
+
37
+ // for (const [key, value] of objA) {
38
+ // if (!Object.is(value, objB.get(key))) {
39
+ // return false
40
+ // }
41
+ // }
42
+ // return true
43
+ // }
44
+
45
+ // if (objA instanceof Set && objB instanceof Set) {
46
+ // if (objA.size !== objB.size) return false
47
+
48
+ // for (const value of objA) {
49
+ // if (!objB.has(value)) {
50
+ // return false
51
+ // }
52
+ // }
53
+ // return true
54
+ // }
55
+
56
+ const keysA = Object.keys(objA)
57
+ if (keysA.length !== Object.keys(objB).length) {
58
+ return false
59
+ }
60
+
61
+ for (let i = 0; i < keysA.length; i++) {
62
+ if (
63
+ !Object.prototype.hasOwnProperty.call(objB, keysA[i] as string) ||
64
+ !Object.is(objA[keysA[i] as keyof T], objB[keysA[i] as keyof T])
65
+ ) {
66
+ return false
67
+ }
68
+ }
69
+ return true
70
+ }
@@ -1,16 +0,0 @@
1
- /**
2
- * Copyright (c) Meta Platforms, Inc. and affiliates.
3
- *
4
- * This source code is licensed under the MIT license found in the
5
- * LICENSE file in the root directory of this source tree.
6
- *
7
- * @flow
8
- */
9
-
10
- 'use strict'
11
-
12
- // Intentionally not using named imports because Rollup uses dynamic
13
- // dispatch for CommonJS interop named imports.
14
- import * as React from 'react'
15
-
16
- export const useSyncExternalStore = React.useSyncExternalStore