@tanstack/vue-router 1.167.5 → 1.168.0

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.
Files changed (95) hide show
  1. package/dist/esm/Match.js +55 -61
  2. package/dist/esm/Match.js.map +1 -1
  3. package/dist/esm/Matches.js +8 -15
  4. package/dist/esm/Matches.js.map +1 -1
  5. package/dist/esm/Scripts.js +7 -6
  6. package/dist/esm/Scripts.js.map +1 -1
  7. package/dist/esm/Transitioner.js +18 -24
  8. package/dist/esm/Transitioner.js.map +1 -1
  9. package/dist/esm/headContentUtils.js +13 -15
  10. package/dist/esm/headContentUtils.js.map +1 -1
  11. package/dist/esm/index.dev.js +6 -6
  12. package/dist/esm/index.js +6 -6
  13. package/dist/esm/link.js +242 -178
  14. package/dist/esm/link.js.map +1 -1
  15. package/dist/esm/matchContext.d.ts +8 -14
  16. package/dist/esm/matchContext.js +11 -9
  17. package/dist/esm/matchContext.js.map +1 -1
  18. package/dist/esm/not-found.js +6 -3
  19. package/dist/esm/not-found.js.map +1 -1
  20. package/dist/esm/router.js +2 -1
  21. package/dist/esm/router.js.map +1 -1
  22. package/dist/esm/routerStores.d.ts +13 -0
  23. package/dist/esm/routerStores.js +33 -0
  24. package/dist/esm/routerStores.js.map +1 -0
  25. package/dist/esm/ssr/RouterClient.js +1 -1
  26. package/dist/esm/ssr/RouterClient.js.map +1 -1
  27. package/dist/esm/ssr/renderRouterToStream.js +2 -2
  28. package/dist/esm/ssr/renderRouterToStream.js.map +1 -1
  29. package/dist/esm/ssr/renderRouterToString.js +1 -1
  30. package/dist/esm/ssr/renderRouterToString.js.map +1 -1
  31. package/dist/esm/useCanGoBack.d.ts +1 -1
  32. package/dist/esm/useCanGoBack.js +3 -2
  33. package/dist/esm/useCanGoBack.js.map +1 -1
  34. package/dist/esm/useLocation.js +3 -2
  35. package/dist/esm/useLocation.js.map +1 -1
  36. package/dist/esm/useMatch.js +29 -19
  37. package/dist/esm/useMatch.js.map +1 -1
  38. package/dist/esm/useRouterState.js +4 -4
  39. package/dist/esm/useRouterState.js.map +1 -1
  40. package/dist/source/Match.jsx +121 -159
  41. package/dist/source/Match.jsx.map +1 -1
  42. package/dist/source/Matches.jsx +11 -28
  43. package/dist/source/Matches.jsx.map +1 -1
  44. package/dist/source/Scripts.jsx +32 -35
  45. package/dist/source/Scripts.jsx.map +1 -1
  46. package/dist/source/Transitioner.jsx +19 -21
  47. package/dist/source/Transitioner.jsx.map +1 -1
  48. package/dist/source/headContentUtils.jsx +51 -61
  49. package/dist/source/headContentUtils.jsx.map +1 -1
  50. package/dist/source/link.jsx +298 -249
  51. package/dist/source/link.jsx.map +1 -1
  52. package/dist/source/matchContext.d.ts +8 -14
  53. package/dist/source/matchContext.jsx +17 -23
  54. package/dist/source/matchContext.jsx.map +1 -1
  55. package/dist/source/not-found.jsx +6 -5
  56. package/dist/source/not-found.jsx.map +1 -1
  57. package/dist/source/router.js +2 -1
  58. package/dist/source/router.js.map +1 -1
  59. package/dist/source/routerStores.d.ts +13 -0
  60. package/dist/source/routerStores.js +37 -0
  61. package/dist/source/routerStores.js.map +1 -0
  62. package/dist/source/ssr/RouterClient.jsx +1 -1
  63. package/dist/source/ssr/RouterClient.jsx.map +1 -1
  64. package/dist/source/ssr/renderRouterToStream.jsx +2 -2
  65. package/dist/source/ssr/renderRouterToStream.jsx.map +1 -1
  66. package/dist/source/ssr/renderRouterToString.jsx +1 -1
  67. package/dist/source/ssr/renderRouterToString.jsx.map +1 -1
  68. package/dist/source/useCanGoBack.d.ts +1 -1
  69. package/dist/source/useCanGoBack.js +4 -2
  70. package/dist/source/useCanGoBack.js.map +1 -1
  71. package/dist/source/useLocation.jsx +4 -4
  72. package/dist/source/useLocation.jsx.map +1 -1
  73. package/dist/source/useMatch.jsx +60 -38
  74. package/dist/source/useMatch.jsx.map +1 -1
  75. package/dist/source/useRouterState.jsx +4 -4
  76. package/dist/source/useRouterState.jsx.map +1 -1
  77. package/package.json +2 -2
  78. package/skills/vue-router/SKILL.md +3 -0
  79. package/src/Match.tsx +168 -180
  80. package/src/Matches.tsx +18 -31
  81. package/src/Scripts.tsx +40 -40
  82. package/src/Transitioner.tsx +35 -23
  83. package/src/headContentUtils.tsx +101 -107
  84. package/src/link.tsx +445 -300
  85. package/src/matchContext.tsx +23 -25
  86. package/src/not-found.tsx +9 -5
  87. package/src/router.ts +2 -1
  88. package/src/routerStores.ts +54 -0
  89. package/src/ssr/RouterClient.tsx +1 -1
  90. package/src/ssr/renderRouterToStream.tsx +2 -2
  91. package/src/ssr/renderRouterToString.tsx +1 -1
  92. package/src/useCanGoBack.ts +7 -2
  93. package/src/useLocation.tsx +8 -5
  94. package/src/useMatch.tsx +95 -49
  95. package/src/useRouterState.tsx +6 -4
@@ -1,41 +1,39 @@
1
1
  import * as Vue from 'vue'
2
2
 
3
- // Create a typed injection key with support for undefined values
4
- // This is the primary match context used throughout the router
3
+ // Reactive nearest-match context used by hooks that work relative to the
4
+ // current match in the tree.
5
5
  export const matchContext = Symbol('TanStackRouterMatch') as Vue.InjectionKey<
6
6
  Vue.Ref<string | undefined>
7
7
  >
8
8
 
9
- // Dummy match context for when we want to look up by explicit 'from' route
10
- export const dummyMatchContext = Symbol(
11
- 'TanStackRouterDummyMatch',
12
- ) as Vue.InjectionKey<Vue.Ref<string | undefined>>
9
+ // Pending match context for nearest-match lookups
10
+ export const pendingMatchContext = Symbol(
11
+ 'TanStackRouterPendingMatch',
12
+ ) as Vue.InjectionKey<Vue.Ref<boolean>>
13
13
 
14
- /**
15
- * Provides a match ID to child components
16
- */
17
- export function provideMatch(matchId: string | undefined) {
18
- Vue.provide(matchContext, Vue.ref(matchId))
19
- }
14
+ // Dummy pending context when nearest pending state is not needed
15
+ export const dummyPendingMatchContext = Symbol(
16
+ 'TanStackRouterDummyPendingMatch',
17
+ ) as Vue.InjectionKey<Vue.Ref<boolean>>
20
18
 
21
- /**
22
- * Retrieves the match ID from the component tree
23
- */
24
- export function injectMatch(): Vue.Ref<string | undefined> {
25
- return Vue.inject(matchContext, Vue.ref(undefined))
26
- }
19
+ // Stable routeId context — a plain string (not reactive) that identifies
20
+ // which route this component belongs to. Provided by Match, consumed by
21
+ // MatchInner, Outlet, and useMatch for routeId-based store lookups.
22
+ export const routeIdContext = Symbol(
23
+ 'TanStackRouterRouteId',
24
+ ) as Vue.InjectionKey<string>
27
25
 
28
26
  /**
29
- * Provides a dummy match ID to child components
27
+ * Retrieves nearest pending-match state from the component tree
30
28
  */
31
- export function provideDummyMatch(matchId: string | undefined) {
32
- Vue.provide(dummyMatchContext, Vue.ref(matchId))
29
+ export function injectPendingMatch(): Vue.Ref<boolean> {
30
+ return Vue.inject(pendingMatchContext, Vue.ref(false))
33
31
  }
34
32
 
35
33
  /**
36
- * Retrieves the dummy match ID from the component tree
37
- * This only exists so we can conditionally inject a value when we are not interested in the nearest match
34
+ * Retrieves dummy pending-match state from the component tree
35
+ * This only exists so we can conditionally inject a value when we are not interested in the nearest pending match
38
36
  */
39
- export function injectDummyMatch(): Vue.Ref<string | undefined> {
40
- return Vue.inject(dummyMatchContext, Vue.ref(undefined))
37
+ export function injectDummyPendingMatch(): Vue.Ref<boolean> {
38
+ return Vue.inject(dummyPendingMatchContext, Vue.ref(false))
41
39
  }
package/src/not-found.tsx CHANGED
@@ -1,7 +1,8 @@
1
1
  import * as Vue from 'vue'
2
2
  import { isNotFound } from '@tanstack/router-core'
3
+ import { useStore } from '@tanstack/vue-store'
3
4
  import { CatchBoundary } from './CatchBoundary'
4
- import { useRouterState } from './useRouterState'
5
+ import { useRouter } from './useRouter'
5
6
  import type { ErrorComponentProps, NotFoundError } from '@tanstack/router-core'
6
7
 
7
8
  export function CatchNotFound(props: {
@@ -9,10 +10,13 @@ export function CatchNotFound(props: {
9
10
  onCatch?: (error: Error) => void
10
11
  children: Vue.VNode
11
12
  }) {
13
+ const router = useRouter()
12
14
  // TODO: Some way for the user to programmatically reset the not-found boundary?
13
- const resetKey = useRouterState({
14
- select: (s) => `not-found-${s.location.pathname}-${s.status}`,
15
- })
15
+ const pathname = useStore(
16
+ router.stores.location,
17
+ (location) => location.pathname,
18
+ )
19
+ const status = useStore(router.stores.status, (value) => value)
16
20
 
17
21
  // Create a function that returns a VNode to match the SyncRouteComponent signature
18
22
  const errorComponentFn = (componentProps: ErrorComponentProps) => {
@@ -32,7 +36,7 @@ export function CatchNotFound(props: {
32
36
  }
33
37
 
34
38
  return Vue.h(CatchBoundary, {
35
- getResetKey: () => resetKey.value,
39
+ getResetKey: () => `not-found-${pathname.value}-${status.value}`,
36
40
  onCatch: (error: Error) => {
37
41
  if (isNotFound(error)) {
38
42
  if (props.onCatch) {
package/src/router.ts CHANGED
@@ -1,4 +1,5 @@
1
1
  import { RouterCore } from '@tanstack/router-core'
2
+ import { getStoreFactory } from './routerStores'
2
3
  import type { RouterHistory } from '@tanstack/history'
3
4
  import type {
4
5
  AnyRoute,
@@ -98,6 +99,6 @@ export class Router<
98
99
  TDehydrated
99
100
  >,
100
101
  ) {
101
- super(options)
102
+ super(options, getStoreFactory)
102
103
  }
103
104
  }
@@ -0,0 +1,54 @@
1
+ import { batch, createStore } from '@tanstack/vue-store'
2
+ import type {
3
+ AnyRoute,
4
+ GetStoreConfig,
5
+ RouterStores,
6
+ } from '@tanstack/router-core'
7
+ import type { Readable } from '@tanstack/vue-store'
8
+
9
+ declare module '@tanstack/router-core' {
10
+ export interface RouterReadableStore<TValue> extends Readable<TValue> {}
11
+ export interface RouterStores<in out TRouteTree extends AnyRoute> {
12
+ /** Maps each active routeId to the matchId of its child in the match tree. */
13
+ childMatchIdByRouteId: RouterReadableStore<Record<string, string>>
14
+ /** Maps each pending routeId to true for quick lookup. */
15
+ pendingRouteIds: RouterReadableStore<Record<string, boolean>>
16
+ }
17
+ }
18
+
19
+ export const getStoreFactory: GetStoreConfig = (_opts) => {
20
+ return {
21
+ createMutableStore: createStore,
22
+ createReadonlyStore: createStore,
23
+ batch,
24
+ init: (stores: RouterStores<AnyRoute>) => {
25
+ // Single derived store: one reactive node that maps every active
26
+ // routeId to its child's matchId. Depends only on matchesId +
27
+ // the pool's routeId tags (which are set during reconciliation).
28
+ // Outlet reads the map and then does a direct pool lookup.
29
+ stores.childMatchIdByRouteId = createStore(() => {
30
+ const ids = stores.matchesId.state
31
+ const obj: Record<string, string> = {}
32
+ for (let i = 0; i < ids.length - 1; i++) {
33
+ const parentStore = stores.activeMatchStoresById.get(ids[i]!)
34
+ if (parentStore?.routeId) {
35
+ obj[parentStore.routeId] = ids[i + 1]!
36
+ }
37
+ }
38
+ return obj
39
+ })
40
+
41
+ stores.pendingRouteIds = createStore(() => {
42
+ const ids = stores.pendingMatchesId.state
43
+ const obj: Record<string, boolean> = {}
44
+ for (const id of ids) {
45
+ const store = stores.pendingMatchStoresById.get(id)
46
+ if (store?.routeId) {
47
+ obj[store.routeId] = true
48
+ }
49
+ }
50
+ return obj
51
+ })
52
+ },
53
+ }
54
+ }
@@ -18,7 +18,7 @@ export const RouterClient = Vue.defineComponent({
18
18
  const isHydrated = Vue.ref(false)
19
19
 
20
20
  if (!hydrationPromise) {
21
- if (!props.router.state.matches.length) {
21
+ if (!props.router.stores.matchesId.state.length) {
22
22
  hydrationPromise = hydrate(props.router)
23
23
  } else {
24
24
  hydrationPromise = Promise.resolve()
@@ -62,7 +62,7 @@ export const renderRouterToStream = async ({
62
62
  }
63
63
 
64
64
  return new Response(`<!DOCTYPE html>${fullHtml}`, {
65
- status: router.state.statusCode,
65
+ status: router.stores.statusCode.state,
66
66
  headers: responseHeaders,
67
67
  })
68
68
  }
@@ -78,7 +78,7 @@ export const renderRouterToStream = async ({
78
78
  )
79
79
 
80
80
  return new Response(responseStream as any, {
81
- status: router.state.statusCode,
81
+ status: router.stores.statusCode.state,
82
82
  headers: responseHeaders,
83
83
  })
84
84
  }
@@ -24,7 +24,7 @@ export const renderRouterToString = async ({
24
24
  }
25
25
 
26
26
  return new Response(`<!DOCTYPE html>${html}`, {
27
- status: router.state.statusCode,
27
+ status: router.stores.statusCode.state,
28
28
  headers: responseHeaders,
29
29
  })
30
30
  } catch (error) {
@@ -1,5 +1,10 @@
1
- import { useRouterState } from './useRouterState'
1
+ import { useStore } from '@tanstack/vue-store'
2
+ import { useRouter } from './useRouter'
2
3
 
3
4
  export function useCanGoBack() {
4
- return useRouterState({ select: (s) => s.location.state.__TSR_index !== 0 })
5
+ const router = useRouter()
6
+ return useStore(
7
+ router.stores.location,
8
+ (location) => location.state.__TSR_index !== 0,
9
+ )
5
10
  }
@@ -1,4 +1,5 @@
1
- import { useRouterState } from './useRouterState'
1
+ import { useStore } from '@tanstack/vue-store'
2
+ import { useRouter } from './useRouter'
2
3
  import type {
3
4
  AnyRouter,
4
5
  RegisteredRouter,
@@ -23,8 +24,10 @@ export function useLocation<
23
24
  >(
24
25
  opts?: UseLocationBaseOptions<TRouter, TSelected>,
25
26
  ): Vue.Ref<UseLocationResult<TRouter, TSelected>> {
26
- return useRouterState({
27
- select: (state: any) =>
28
- opts?.select ? opts.select(state.location) : state.location,
29
- } as any) as Vue.Ref<UseLocationResult<TRouter, TSelected>>
27
+ const router = useRouter<TRouter>()
28
+ return useStore(
29
+ router.stores.location,
30
+ (location) =>
31
+ (opts?.select ? opts.select(location as any) : location) as any,
32
+ ) as Vue.Ref<UseLocationResult<TRouter, TSelected>>
30
33
  }
package/src/useMatch.tsx CHANGED
@@ -1,6 +1,13 @@
1
1
  import * as Vue from 'vue'
2
- import { useRouterState } from './useRouterState'
3
- import { injectDummyMatch, injectMatch } from './matchContext'
2
+ import { useStore } from '@tanstack/vue-store'
3
+ import { isServer } from '@tanstack/router-core/isServer'
4
+ import invariant from 'tiny-invariant'
5
+ import {
6
+ injectDummyPendingMatch,
7
+ injectPendingMatch,
8
+ routeIdContext,
9
+ } from './matchContext'
10
+ import { useRouter } from './useRouter'
4
11
  import type {
5
12
  AnyRouter,
6
13
  MakeRouteMatch,
@@ -68,60 +75,99 @@ export function useMatch<
68
75
  ): Vue.Ref<
69
76
  ThrowOrOptional<UseMatchResult<TRouter, TFrom, TStrict, TSelected>, TThrow>
70
77
  > {
71
- const nearestMatchId = opts.from ? injectDummyMatch() : injectMatch()
78
+ const router = useRouter<TRouter>()
72
79
 
73
- // Store to track pending error for deferred throwing
74
- const pendingError = Vue.ref<Error | null>(null)
80
+ // During SSR we render exactly once and do not need reactivity.
81
+ // Avoid store subscriptions and pending/transition bookkeeping on the server.
82
+ if (isServer ?? router.isServer) {
83
+ const nearestRouteId = opts.from ? undefined : Vue.inject(routeIdContext)
84
+ const matchStore =
85
+ (opts.from ?? nearestRouteId)
86
+ ? router.stores.getMatchStoreByRouteId(opts.from ?? nearestRouteId!)
87
+ : undefined
88
+ const match = matchStore?.state
75
89
 
76
- // Select the match from router state
77
- const matchSelection = useRouterState({
78
- select: (state: any) => {
79
- const match = state.matches.find((d: any) =>
80
- opts.from ? opts.from === d.routeId : d.id === nearestMatchId.value,
90
+ invariant(
91
+ !((opts.shouldThrow ?? true) && !match),
92
+ `Could not find ${opts.from ? `an active match from "${opts.from}"` : 'a nearest match!'}`,
93
+ )
94
+
95
+ if (match === undefined) {
96
+ return Vue.ref(undefined) as Vue.Ref<
97
+ ThrowOrOptional<
98
+ UseMatchResult<TRouter, TFrom, TStrict, TSelected>,
99
+ TThrow
100
+ >
101
+ >
102
+ }
103
+
104
+ return Vue.ref(opts.select ? opts.select(match) : match) as Vue.Ref<
105
+ ThrowOrOptional<
106
+ UseMatchResult<TRouter, TFrom, TStrict, TSelected>,
107
+ TThrow
108
+ >
109
+ >
110
+ }
111
+
112
+ const hasPendingNearestMatch = opts.from
113
+ ? injectDummyPendingMatch()
114
+ : injectPendingMatch()
115
+ // Set up reactive match value based on lookup strategy.
116
+ let match: Readonly<Vue.Ref<any>>
117
+
118
+ if (opts.from) {
119
+ // routeId case: single subscription via per-routeId computed store.
120
+ // The store reference is stable (cached by routeId).
121
+ const matchStore = router.stores.getMatchStoreByRouteId(opts.from)
122
+ match = useStore(matchStore, (value) => value)
123
+ } else {
124
+ // matchId case: use routeId from context for stable store lookup.
125
+ // The routeId is provided by the nearest Match component and doesn't
126
+ // change for the component's lifetime, so the store is stable.
127
+ const nearestRouteId = Vue.inject(routeIdContext)
128
+ if (nearestRouteId) {
129
+ match = useStore(
130
+ router.stores.getMatchStoreByRouteId(nearestRouteId),
131
+ (value) => value,
81
132
  )
133
+ } else {
134
+ // No route context — will fall through to error handling below
135
+ match = Vue.ref(undefined) as Readonly<Vue.Ref<undefined>>
136
+ }
137
+ }
138
+
139
+ const hasPendingRouteMatch = opts.from
140
+ ? useStore(router.stores.pendingRouteIds, (ids) => ids)
141
+ : undefined
142
+ const isTransitioning = useStore(
143
+ router.stores.isTransitioning,
144
+ (value) => value,
145
+ { equal: Object.is },
146
+ )
82
147
 
83
- if (match === undefined) {
84
- // During navigation transitions, check if the match exists in pendingMatches
85
- const pendingMatch = state.pendingMatches?.find((d: any) =>
86
- opts.from ? opts.from === d.routeId : d.id === nearestMatchId.value,
87
- )
88
-
89
- // If there's a pending match or we're transitioning, return undefined without throwing
90
- if (pendingMatch || state.isTransitioning) {
91
- pendingError.value = null
92
- return undefined
93
- }
94
-
95
- // Store the error to throw later if shouldThrow is enabled
96
- if (opts.shouldThrow ?? true) {
97
- pendingError.value = new Error(
98
- `Invariant failed: Could not find ${opts.from ? `an active match from "${opts.from}"` : 'a nearest match!'}`,
99
- )
100
- }
101
-
102
- return undefined
103
- }
104
-
105
- pendingError.value = null
106
- return opts.select ? opts.select(match) : match
107
- },
108
- } as any)
109
-
110
- // Throw the error if we have one - this happens after the selector runs
111
- // Using a computed so the error is thrown when the return value is accessed
112
148
  const result = Vue.computed(() => {
113
- // Check for pending error first
114
- if (pendingError.value) {
115
- throw pendingError.value
149
+ const selectedMatch = match.value
150
+ if (selectedMatch === undefined) {
151
+ const hasPendingMatch = opts.from
152
+ ? Boolean(hasPendingRouteMatch?.value[opts.from!])
153
+ : hasPendingNearestMatch.value
154
+ invariant(
155
+ !(
156
+ !hasPendingMatch &&
157
+ !isTransitioning.value &&
158
+ (opts.shouldThrow ?? true)
159
+ ),
160
+ `Could not find ${opts.from ? `an active match from "${opts.from}"` : 'a nearest match!'}`,
161
+ )
162
+
163
+ return undefined
116
164
  }
117
- return matchSelection.value
165
+
166
+ return opts.select ? opts.select(selectedMatch) : selectedMatch
118
167
  })
119
168
 
120
- // Also immediately throw if there's already an error from initial render
121
- // This ensures errors are thrown even if the returned ref is never accessed
122
- if (pendingError.value) {
123
- throw pendingError.value
124
- }
169
+ // Keep eager throw behavior for setups that call useMatch for side effects only.
170
+ result.value
125
171
 
126
- return result as any
172
+ return result
127
173
  }
@@ -1,6 +1,6 @@
1
- import { useStore } from '@tanstack/vue-store'
2
1
  import * as Vue from 'vue'
3
2
  import { isServer } from '@tanstack/router-core/isServer'
3
+ import { useStore } from '@tanstack/vue-store'
4
4
  import { useRouter } from './useRouter'
5
5
  import type {
6
6
  AnyRouter,
@@ -30,7 +30,7 @@ export function useRouterState<
30
30
  const router = opts?.router || contextRouter
31
31
 
32
32
  // Return a safe default if router is undefined
33
- if (!router || !router.__store) {
33
+ if (!router || !router.stores.__store) {
34
34
  return Vue.ref(undefined) as Vue.Ref<
35
35
  UseRouterStateResult<TRouter, TSelected>
36
36
  >
@@ -42,13 +42,15 @@ export function useRouterState<
42
42
  const _isServer = isServer ?? router.isServer
43
43
 
44
44
  if (_isServer) {
45
- const state = router.state as RouterState<TRouter['routeTree']>
45
+ const state = router.stores.__store.state as RouterState<
46
+ TRouter['routeTree']
47
+ >
46
48
  return Vue.ref(opts?.select ? opts.select(state) : state) as Vue.Ref<
47
49
  UseRouterStateResult<TRouter, TSelected>
48
50
  >
49
51
  }
50
52
 
51
- return useStore(router.__store, (state) => {
53
+ return useStore(router.stores.__store, (state) => {
52
54
  if (opts?.select) return opts.select(state)
53
55
 
54
56
  return state