@tanstack/vue-router 1.140.1 → 1.141.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 (150) hide show
  1. package/dist/esm/Asset.js +122 -8
  2. package/dist/esm/Asset.js.map +1 -1
  3. package/dist/esm/Body.d.ts +4 -0
  4. package/dist/esm/Body.js +26 -0
  5. package/dist/esm/Body.js.map +1 -0
  6. package/dist/esm/CatchBoundary.d.ts +1 -1
  7. package/dist/esm/CatchBoundary.js +8 -8
  8. package/dist/esm/CatchBoundary.js.map +1 -1
  9. package/dist/esm/Html.d.ts +4 -0
  10. package/dist/esm/Html.js +63 -0
  11. package/dist/esm/Html.js.map +1 -0
  12. package/dist/esm/Match.js +87 -49
  13. package/dist/esm/Match.js.map +1 -1
  14. package/dist/esm/Matches.js +3 -2
  15. package/dist/esm/Matches.js.map +1 -1
  16. package/dist/esm/RouterProvider.js +3 -0
  17. package/dist/esm/RouterProvider.js.map +1 -1
  18. package/dist/esm/ScriptOnce.d.ts +12 -5
  19. package/dist/esm/ScriptOnce.js +35 -15
  20. package/dist/esm/ScriptOnce.js.map +1 -1
  21. package/dist/esm/Scripts.d.ts +2 -1
  22. package/dist/esm/Scripts.js +101 -35
  23. package/dist/esm/Scripts.js.map +1 -1
  24. package/dist/esm/Transitioner.d.ts +16 -0
  25. package/dist/esm/Transitioner.js +136 -133
  26. package/dist/esm/Transitioner.js.map +1 -1
  27. package/dist/esm/awaited.d.ts +20 -5
  28. package/dist/esm/awaited.js +17 -20
  29. package/dist/esm/awaited.js.map +1 -1
  30. package/dist/esm/index.d.ts +2 -0
  31. package/dist/esm/index.js +4 -0
  32. package/dist/esm/index.js.map +1 -1
  33. package/dist/esm/lazyRouteComponent.js +2 -2
  34. package/dist/esm/lazyRouteComponent.js.map +1 -1
  35. package/dist/esm/link.js +27 -35
  36. package/dist/esm/link.js.map +1 -1
  37. package/dist/esm/scroll-restoration.d.ts +8 -1
  38. package/dist/esm/scroll-restoration.js +44 -12
  39. package/dist/esm/scroll-restoration.js.map +1 -1
  40. package/dist/esm/ssr/RouterClient.d.ts +15 -0
  41. package/dist/esm/ssr/RouterClient.js +46 -0
  42. package/dist/esm/ssr/RouterClient.js.map +1 -0
  43. package/dist/esm/ssr/RouterServer.d.ts +15 -0
  44. package/dist/esm/ssr/RouterServer.js +37 -0
  45. package/dist/esm/ssr/RouterServer.js.map +1 -0
  46. package/dist/esm/ssr/client.d.ts +1 -0
  47. package/dist/esm/ssr/client.js +5 -0
  48. package/dist/esm/ssr/client.js.map +1 -0
  49. package/dist/esm/ssr/defaultRenderHandler.d.ts +1 -0
  50. package/dist/esm/ssr/defaultRenderHandler.js +15 -0
  51. package/dist/esm/ssr/defaultRenderHandler.js.map +1 -0
  52. package/dist/esm/ssr/defaultStreamHandler.d.ts +1 -0
  53. package/dist/esm/ssr/defaultStreamHandler.js +17 -0
  54. package/dist/esm/ssr/defaultStreamHandler.js.map +1 -0
  55. package/dist/esm/ssr/renderRouterToStream.d.ts +8 -0
  56. package/dist/esm/ssr/renderRouterToStream.js +70 -0
  57. package/dist/esm/ssr/renderRouterToStream.js.map +1 -0
  58. package/dist/esm/ssr/renderRouterToString.d.ts +7 -0
  59. package/dist/esm/ssr/renderRouterToString.js +33 -0
  60. package/dist/esm/ssr/renderRouterToString.js.map +1 -0
  61. package/dist/esm/ssr/server.d.ts +6 -0
  62. package/dist/esm/ssr/server.js +14 -0
  63. package/dist/esm/ssr/server.js.map +1 -0
  64. package/dist/source/Asset.jsx +119 -7
  65. package/dist/source/Asset.jsx.map +1 -1
  66. package/dist/source/Body.d.ts +4 -0
  67. package/dist/source/Body.jsx +15 -0
  68. package/dist/source/Body.jsx.map +1 -0
  69. package/dist/source/CatchBoundary.d.ts +1 -1
  70. package/dist/source/CatchBoundary.jsx +10 -23
  71. package/dist/source/CatchBoundary.jsx.map +1 -1
  72. package/dist/source/Html.d.ts +4 -0
  73. package/dist/source/Html.jsx +56 -0
  74. package/dist/source/Html.jsx.map +1 -0
  75. package/dist/source/Match.jsx +119 -54
  76. package/dist/source/Match.jsx.map +1 -1
  77. package/dist/source/Matches.jsx +15 -3
  78. package/dist/source/Matches.jsx.map +1 -1
  79. package/dist/source/RouterProvider.jsx +5 -0
  80. package/dist/source/RouterProvider.jsx.map +1 -1
  81. package/dist/source/ScriptOnce.d.ts +12 -5
  82. package/dist/source/ScriptOnce.jsx +27 -16
  83. package/dist/source/ScriptOnce.jsx.map +1 -1
  84. package/dist/source/Scripts.d.ts +2 -1
  85. package/dist/source/Scripts.jsx +100 -42
  86. package/dist/source/Scripts.jsx.map +1 -1
  87. package/dist/source/Transitioner.d.ts +16 -0
  88. package/dist/source/Transitioner.jsx +180 -160
  89. package/dist/source/Transitioner.jsx.map +1 -1
  90. package/dist/source/awaited.d.ts +20 -5
  91. package/dist/source/awaited.jsx +18 -25
  92. package/dist/source/awaited.jsx.map +1 -1
  93. package/dist/source/index.d.ts +2 -0
  94. package/dist/source/index.jsx +2 -0
  95. package/dist/source/index.jsx.map +1 -1
  96. package/dist/source/lazyRouteComponent.jsx +4 -2
  97. package/dist/source/lazyRouteComponent.jsx.map +1 -1
  98. package/dist/source/link.jsx +37 -51
  99. package/dist/source/link.jsx.map +1 -1
  100. package/dist/source/scroll-restoration.d.ts +8 -1
  101. package/dist/source/scroll-restoration.jsx +55 -12
  102. package/dist/source/scroll-restoration.jsx.map +1 -1
  103. package/dist/source/ssr/RouterClient.d.ts +15 -0
  104. package/dist/source/ssr/RouterClient.jsx +48 -0
  105. package/dist/source/ssr/RouterClient.jsx.map +1 -0
  106. package/dist/source/ssr/RouterServer.d.ts +15 -0
  107. package/dist/source/ssr/RouterServer.jsx +40 -0
  108. package/dist/source/ssr/RouterServer.jsx.map +1 -0
  109. package/dist/source/ssr/client.d.ts +1 -0
  110. package/dist/source/ssr/client.js +2 -0
  111. package/dist/source/ssr/client.js.map +1 -0
  112. package/dist/source/ssr/defaultRenderHandler.d.ts +1 -0
  113. package/dist/source/ssr/defaultRenderHandler.jsx +9 -0
  114. package/dist/source/ssr/defaultRenderHandler.jsx.map +1 -0
  115. package/dist/source/ssr/defaultStreamHandler.d.ts +1 -0
  116. package/dist/source/ssr/defaultStreamHandler.jsx +10 -0
  117. package/dist/source/ssr/defaultStreamHandler.jsx.map +1 -0
  118. package/dist/source/ssr/renderRouterToStream.d.ts +8 -0
  119. package/dist/source/ssr/renderRouterToStream.jsx +55 -0
  120. package/dist/source/ssr/renderRouterToStream.jsx.map +1 -0
  121. package/dist/source/ssr/renderRouterToString.d.ts +7 -0
  122. package/dist/source/ssr/renderRouterToString.jsx +26 -0
  123. package/dist/source/ssr/renderRouterToString.jsx.map +1 -0
  124. package/dist/source/ssr/server.d.ts +6 -0
  125. package/dist/source/ssr/server.js +7 -0
  126. package/dist/source/ssr/server.js.map +1 -0
  127. package/package.json +16 -3
  128. package/src/Asset.tsx +157 -7
  129. package/src/Body.tsx +26 -0
  130. package/src/CatchBoundary.tsx +11 -25
  131. package/src/Html.tsx +65 -0
  132. package/src/Match.tsx +135 -58
  133. package/src/Matches.tsx +16 -4
  134. package/src/RouterProvider.tsx +6 -0
  135. package/src/ScriptOnce.tsx +43 -28
  136. package/src/Scripts.tsx +121 -56
  137. package/src/Transitioner.tsx +197 -176
  138. package/src/awaited.tsx +17 -28
  139. package/src/index.tsx +2 -0
  140. package/src/lazyRouteComponent.tsx +4 -2
  141. package/src/link.tsx +42 -47
  142. package/src/scroll-restoration.tsx +69 -21
  143. package/src/ssr/RouterClient.tsx +58 -0
  144. package/src/ssr/RouterServer.tsx +51 -0
  145. package/src/ssr/client.ts +1 -0
  146. package/src/ssr/defaultRenderHandler.tsx +12 -0
  147. package/src/ssr/defaultStreamHandler.tsx +13 -0
  148. package/src/ssr/renderRouterToStream.tsx +85 -0
  149. package/src/ssr/renderRouterToString.tsx +37 -0
  150. package/src/ssr/server.ts +6 -0
@@ -8,206 +8,227 @@ import { useRouter } from './useRouter'
8
8
  import { useRouterState } from './useRouterState'
9
9
  import { usePrevious } from './utils'
10
10
 
11
- export const Transitioner = Vue.defineComponent({
12
- name: 'Transitioner',
13
- setup() {
14
- const router = useRouter()
15
- let mountLoadForRouter = { router, mounted: false }
16
-
17
- if (router.isServer) {
18
- return () => null
11
+ // Track mount state per router to avoid double-loading
12
+ let mountLoadForRouter = { router: null as any, mounted: false }
13
+
14
+ /**
15
+ * Composable that sets up router transition logic.
16
+ * This is called from MatchesContent to set up:
17
+ * - router.startTransition
18
+ * - router.startViewTransition
19
+ * - History subscription
20
+ * - Router event watchers
21
+ *
22
+ * Must be called during component setup phase.
23
+ */
24
+ export function useTransitionerSetup() {
25
+ const router = useRouter()
26
+
27
+ // Skip on server - no transitions needed
28
+ if (router.isServer) {
29
+ return
30
+ }
31
+
32
+ const isLoading = useRouterState({
33
+ select: ({ isLoading }) => isLoading,
34
+ })
35
+
36
+ // Track if we're in a transition - using a ref to track async transitions
37
+ const isTransitioning = Vue.ref(false)
38
+
39
+ // Track pending state changes
40
+ const hasPendingMatches = useRouterState({
41
+ select: (s) => s.matches.some((d) => d.status === 'pending'),
42
+ })
43
+
44
+ const previousIsLoading = usePrevious(() => isLoading.value)
45
+
46
+ const isAnyPending = Vue.computed(
47
+ () => isLoading.value || isTransitioning.value || hasPendingMatches.value,
48
+ )
49
+ const previousIsAnyPending = usePrevious(() => isAnyPending.value)
50
+
51
+ const isPagePending = Vue.computed(
52
+ () => isLoading.value || hasPendingMatches.value,
53
+ )
54
+ const previousIsPagePending = usePrevious(() => isPagePending.value)
55
+
56
+ // Implement startTransition similar to React/Solid
57
+ // Vue doesn't have a native useTransition like React 18, so we simulate it
58
+ // We also update the router state's isTransitioning flag so useMatch can check it
59
+ router.startTransition = (fn: () => void | Promise<void>) => {
60
+ isTransitioning.value = true
61
+ // Also update the router state so useMatch knows we're transitioning
62
+ try {
63
+ router.__store.setState((s) => ({ ...s, isTransitioning: true }))
64
+ } catch {
65
+ // Ignore errors if component is unmounted
19
66
  }
20
67
 
21
- const isLoading = useRouterState({
22
- select: ({ isLoading }) => isLoading,
23
- })
24
-
25
- // Track if we're in a transition - using a ref to track async transitions
26
- const isTransitioning = Vue.ref(false)
68
+ // Helper to end the transition
69
+ const endTransition = () => {
70
+ // Use nextTick to ensure Vue has processed all reactive updates
71
+ Vue.nextTick(() => {
72
+ try {
73
+ isTransitioning.value = false
74
+ router.__store.setState((s) => ({ ...s, isTransitioning: false }))
75
+ } catch {
76
+ // Ignore errors if component is unmounted
77
+ }
78
+ })
79
+ }
27
80
 
28
- // Track pending state changes
29
- const hasPendingMatches = useRouterState({
30
- select: (s) => s.matches.some((d) => d.status === 'pending'),
81
+ // Execute the function synchronously
82
+ // The function internally may call startViewTransition which schedules async work
83
+ // via document.startViewTransition, but we don't need to wait for it here
84
+ // because Vue's reactivity will trigger re-renders when state changes
85
+ fn()
86
+
87
+ // End the transition on next tick to allow Vue to process reactive updates
88
+ endTransition()
89
+ }
90
+
91
+ // For Vue, we need to completely override startViewTransition because Vue's
92
+ // async rendering doesn't work well with the View Transitions API's requirement
93
+ // for synchronous DOM updates. The browser expects the DOM to be updated
94
+ // when the callback promise resolves, but Vue updates asynchronously.
95
+ //
96
+ // Our approach: Skip the actual view transition animation but still update state.
97
+ // This ensures navigation works correctly even without the visual transition.
98
+ // In the future, we could explore using viewTransition.captured like vue-view-transitions does.
99
+ router.startViewTransition = (fn: () => Promise<void>) => {
100
+ // Just run the callback directly without wrapping in document.startViewTransition
101
+ // This ensures the state updates happen and Vue can render them normally
102
+ fn()
103
+ }
104
+
105
+ // Subscribe to location changes
106
+ // and try to load the new location
107
+ let unsubscribe: (() => void) | undefined
108
+
109
+ Vue.onMounted(() => {
110
+ unsubscribe = router.history.subscribe(router.load)
111
+
112
+ const nextLocation = router.buildLocation({
113
+ to: router.latestLocation.pathname,
114
+ search: true,
115
+ params: true,
116
+ hash: true,
117
+ state: true,
118
+ _includeValidateSearch: true,
31
119
  })
32
120
 
33
- const previousIsLoading = usePrevious(() => isLoading.value)
34
-
35
- const isAnyPending = Vue.computed(
36
- () => isLoading.value || isTransitioning.value || hasPendingMatches.value,
37
- )
38
- const previousIsAnyPending = usePrevious(() => isAnyPending.value)
39
-
40
- const isPagePending = Vue.computed(
41
- () => isLoading.value || hasPendingMatches.value,
42
- )
43
- const previousIsPagePending = usePrevious(() => isPagePending.value)
44
-
45
- // Implement startTransition similar to React/Solid
46
- // Vue doesn't have a native useTransition like React 18, so we simulate it
47
- // We also update the router state's isTransitioning flag so useMatch can check it
48
- router.startTransition = (fn: () => void | Promise<void>) => {
49
- isTransitioning.value = true
50
- // Also update the router state so useMatch knows we're transitioning
51
- try {
52
- router.__store.setState((s) => ({ ...s, isTransitioning: true }))
53
- } catch {
54
- // Ignore errors if component is unmounted
55
- }
121
+ if (
122
+ trimPathRight(router.latestLocation.href) !==
123
+ trimPathRight(nextLocation.href)
124
+ ) {
125
+ router.commitLocation({ ...nextLocation, replace: true })
126
+ }
127
+ })
56
128
 
57
- // Helper to end the transition
58
- const endTransition = () => {
59
- // Use nextTick to ensure Vue has processed all reactive updates
60
- Vue.nextTick(() => {
61
- try {
62
- isTransitioning.value = false
63
- router.__store.setState((s) => ({ ...s, isTransitioning: false }))
64
- } catch {
65
- // Ignore errors if component is unmounted
66
- }
67
- })
68
- }
129
+ // Track if component is mounted to prevent updates after unmount
130
+ const isMounted = Vue.ref(false)
69
131
 
70
- // Execute the function synchronously
71
- // The function internally may call startViewTransition which schedules async work
72
- // via document.startViewTransition, but we don't need to wait for it here
73
- // because Vue's reactivity will trigger re-renders when state changes
74
- fn()
132
+ Vue.onMounted(() => {
133
+ isMounted.value = true
134
+ })
75
135
 
76
- // End the transition on next tick to allow Vue to process reactive updates
77
- endTransition()
136
+ Vue.onUnmounted(() => {
137
+ isMounted.value = false
138
+ if (unsubscribe) {
139
+ unsubscribe()
78
140
  }
79
-
80
- // For Vue, we need to completely override startViewTransition because Vue's
81
- // async rendering doesn't work well with the View Transitions API's requirement
82
- // for synchronous DOM updates. The browser expects the DOM to be updated
83
- // when the callback promise resolves, but Vue updates asynchronously.
84
- //
85
- // Our approach: Skip the actual view transition animation but still update state.
86
- // This ensures navigation works correctly even without the visual transition.
87
- // In the future, we could explore using viewTransition.captured like vue-view-transitions does.
88
- router.startViewTransition = (fn: () => Promise<void>) => {
89
- // Just run the callback directly without wrapping in document.startViewTransition
90
- // This ensures the state updates happen and Vue can render them normally
91
- fn()
141
+ })
142
+
143
+ // Try to load the initial location
144
+ Vue.onMounted(() => {
145
+ if (
146
+ (typeof window !== 'undefined' && router.ssr) ||
147
+ (mountLoadForRouter.router === router && mountLoadForRouter.mounted)
148
+ ) {
149
+ return
92
150
  }
93
-
94
- // Subscribe to location changes
95
- // and try to load the new location
96
- let unsubscribe: (() => void) | undefined
97
-
98
- Vue.onMounted(() => {
99
- unsubscribe = router.history.subscribe(router.load)
100
-
101
- const nextLocation = router.buildLocation({
102
- to: router.latestLocation.pathname,
103
- search: true,
104
- params: true,
105
- hash: true,
106
- state: true,
107
- _includeValidateSearch: true,
108
- })
109
-
110
- if (
111
- trimPathRight(router.latestLocation.href) !==
112
- trimPathRight(nextLocation.href)
113
- ) {
114
- router.commitLocation({ ...nextLocation, replace: true })
115
- }
116
- })
117
-
118
- // Track if component is mounted to prevent updates after unmount
119
- const isMounted = Vue.ref(false)
120
-
121
- Vue.onMounted(() => {
122
- isMounted.value = true
123
- })
124
-
125
- Vue.onUnmounted(() => {
126
- isMounted.value = false
127
- if (unsubscribe) {
128
- unsubscribe()
129
- }
130
- })
131
-
132
- // Try to load the initial location
133
- Vue.onMounted(() => {
134
- if (
135
- (typeof window !== 'undefined' && router.ssr) ||
136
- (mountLoadForRouter.router === router && mountLoadForRouter.mounted)
137
- ) {
138
- return
139
- }
140
- mountLoadForRouter = { router, mounted: true }
141
- const tryLoad = async () => {
142
- try {
143
- await router.load()
144
- } catch (err) {
145
- console.error(err)
146
- }
151
+ mountLoadForRouter = { router, mounted: true }
152
+ const tryLoad = async () => {
153
+ try {
154
+ await router.load()
155
+ } catch (err) {
156
+ console.error(err)
147
157
  }
148
- tryLoad()
149
- })
150
-
151
- // Setup watchers for emitting events
152
- // All watchers check isMounted to prevent updates after unmount
153
- Vue.watch(
154
- () => isLoading.value,
155
- (newValue) => {
156
- if (!isMounted.value) return
157
- try {
158
- if (previousIsLoading.value.previous && !newValue) {
159
- router.emit({
160
- type: 'onLoad',
161
- ...getLocationChangeInfo(router.state),
162
- })
163
- }
164
- } catch {
165
- // Ignore errors if component is unmounted
166
- }
167
- },
168
- )
169
-
170
- Vue.watch(isPagePending, (newValue) => {
158
+ }
159
+ tryLoad()
160
+ })
161
+
162
+ // Setup watchers for emitting events
163
+ // All watchers check isMounted to prevent updates after unmount
164
+ Vue.watch(
165
+ () => isLoading.value,
166
+ (newValue) => {
171
167
  if (!isMounted.value) return
172
168
  try {
173
- // emit onBeforeRouteMount
174
- if (previousIsPagePending.value.previous && !newValue) {
169
+ if (previousIsLoading.value.previous && !newValue) {
175
170
  router.emit({
176
- type: 'onBeforeRouteMount',
171
+ type: 'onLoad',
177
172
  ...getLocationChangeInfo(router.state),
178
173
  })
179
174
  }
180
175
  } catch {
181
176
  // Ignore errors if component is unmounted
182
177
  }
183
- })
184
-
185
- Vue.watch(isAnyPending, (newValue) => {
186
- if (!isMounted.value) return
187
- try {
188
- // The router was pending and now it's not
189
- if (previousIsAnyPending.value.previous && !newValue) {
190
- const changeInfo = getLocationChangeInfo(router.state)
191
- router.emit({
192
- type: 'onResolved',
193
- ...changeInfo,
194
- })
178
+ },
179
+ )
180
+
181
+ Vue.watch(isPagePending, (newValue) => {
182
+ if (!isMounted.value) return
183
+ try {
184
+ // emit onBeforeRouteMount
185
+ if (previousIsPagePending.value.previous && !newValue) {
186
+ router.emit({
187
+ type: 'onBeforeRouteMount',
188
+ ...getLocationChangeInfo(router.state),
189
+ })
190
+ }
191
+ } catch {
192
+ // Ignore errors if component is unmounted
193
+ }
194
+ })
195
+
196
+ Vue.watch(isAnyPending, (newValue) => {
197
+ if (!isMounted.value) return
198
+ try {
199
+ // The router was pending and now it's not
200
+ if (previousIsAnyPending.value.previous && !newValue) {
201
+ const changeInfo = getLocationChangeInfo(router.state)
202
+ router.emit({
203
+ type: 'onResolved',
204
+ ...changeInfo,
205
+ })
195
206
 
196
- router.__store.setState((s) => ({
197
- ...s,
198
- status: 'idle',
199
- resolvedLocation: s.location,
200
- }))
207
+ router.__store.setState((s) => ({
208
+ ...s,
209
+ status: 'idle',
210
+ resolvedLocation: s.location,
211
+ }))
201
212
 
202
- if (changeInfo.hrefChanged) {
203
- handleHashScroll(router)
204
- }
213
+ if (changeInfo.hrefChanged) {
214
+ handleHashScroll(router)
205
215
  }
206
- } catch {
207
- // Ignore errors if component is unmounted
208
216
  }
209
- })
210
-
217
+ } catch {
218
+ // Ignore errors if component is unmounted
219
+ }
220
+ })
221
+ }
222
+
223
+ /**
224
+ * @deprecated Use useTransitionerSetup() composable instead.
225
+ * This component is kept for backwards compatibility but the setup logic
226
+ * has been moved to useTransitionerSetup() for better SSR hydration.
227
+ */
228
+ export const Transitioner = Vue.defineComponent({
229
+ name: 'Transitioner',
230
+ setup() {
231
+ useTransitionerSetup()
211
232
  return () => null
212
233
  },
213
234
  })
package/src/awaited.tsx CHANGED
@@ -23,32 +23,21 @@ export function useAwaited<T>({
23
23
  return [promise[TSR_DEFERRED_PROMISE].data, promise]
24
24
  }
25
25
 
26
- export function Await<T>(
27
- props: AwaitOptions<T> & {
28
- fallback?: Vue.VNode
29
- children: (result: T) => Vue.VNode
26
+ export const Await = Vue.defineComponent({
27
+ name: 'Await',
28
+ props: {
29
+ promise: {
30
+ type: Promise,
31
+ required: true,
32
+ },
33
+ children: {
34
+ type: Function,
35
+ required: true,
36
+ },
30
37
  },
31
- ) {
32
- const data = Vue.ref<T | null>(null)
33
- const error = Vue.ref<Error | null>(null)
34
- const pending = Vue.ref(true)
35
-
36
- Vue.watchEffect(async () => {
37
- pending.value = true
38
- try {
39
- data.value = await props.promise
40
- } catch (err) {
41
- error.value = err as Error
42
- } finally {
43
- pending.value = false
44
- }
45
- })
46
-
47
- const inner = Vue.computed(() => {
48
- if (error.value) throw error.value
49
- if (pending.value) return props.fallback
50
- return props.children(data.value as T)
51
- })
52
-
53
- return () => inner.value
54
- }
38
+ async setup(props) {
39
+ const deferred = defer(props.promise)
40
+ const data = await deferred
41
+ return () => (props.children as (result: unknown) => Vue.VNode)(data)
42
+ },
43
+ })
package/src/index.tsx CHANGED
@@ -339,6 +339,8 @@ export { ScriptOnce } from './ScriptOnce'
339
339
  export { Asset } from './Asset'
340
340
  export { HeadContent } from './HeadContent'
341
341
  export { Scripts } from './Scripts'
342
+ export { Body } from './Body'
343
+ export { Html } from './Html'
342
344
  export { composeRewrites } from '@tanstack/router-core'
343
345
  export type {
344
346
  LocationRewrite,
@@ -98,7 +98,8 @@ export function lazyRouteComponent<
98
98
  name: 'LazyRouteComponent',
99
99
  setup(props: any) {
100
100
  // Create refs to track component state
101
- const component = Vue.ref<any>(comp)
101
+ // Use shallowRef for component to avoid making it reactive (Vue warning)
102
+ const component = Vue.shallowRef<any>(comp ? Vue.markRaw(comp) : comp)
102
103
  const errorState = Vue.ref<any>(error)
103
104
  const loading = Vue.ref(!component.value && !errorState.value)
104
105
 
@@ -109,7 +110,8 @@ export function lazyRouteComponent<
109
110
 
110
111
  load()
111
112
  .then((result) => {
112
- component.value = result
113
+ // Use markRaw to prevent Vue from making the component reactive
114
+ component.value = result ? Vue.markRaw(result) : result
113
115
  loading.value = false
114
116
  })
115
117
  .catch((err) => {
package/src/link.tsx CHANGED
@@ -446,13 +446,8 @@ export function useLinkProps<
446
446
  return hrefValue
447
447
  })
448
448
 
449
- // Create a reactive proxy that reads computed values on access
450
- // This allows the returned object to stay reactive when used in templates
451
- // Use shallowReactive to preserve the ref object without unwrapping it
452
- const reactiveProps: HTMLAttributes = Vue.shallowReactive({
453
- ...getPropsSafeToSpread(),
454
- href: undefined as string | undefined,
455
- ref,
449
+ // Create static event handlers that don't change between renders
450
+ const staticEventHandlers = {
456
451
  onClick: composeEventHandlers<MouseEvent>([
457
452
  options.onClick,
458
453
  handleClick,
@@ -481,72 +476,67 @@ export function useLinkProps<
481
476
  options.onTouchStart,
482
477
  handleTouchStart,
483
478
  ]) as any,
484
- disabled: !!options.disabled,
485
- target: options.target,
486
- })
487
-
488
- // Watch computed values and update reactive props
489
- Vue.watchEffect(() => {
490
- // Update from resolved active/inactive props
491
- const activeP = resolvedActiveProps.value
492
- const inactiveP = resolvedInactiveProps.value
479
+ }
493
480
 
494
- // Update href
495
- reactiveProps.href = href.value
481
+ // Compute all props synchronously to avoid hydration mismatches
482
+ // Using Vue.computed ensures props are calculated at render time, not after
483
+ const computedProps = Vue.computed<HTMLAttributes>(() => {
484
+ const result: HTMLAttributes = {
485
+ ...getPropsSafeToSpread(),
486
+ href: href.value,
487
+ ref,
488
+ ...staticEventHandlers,
489
+ disabled: !!options.disabled,
490
+ target: options.target,
491
+ }
496
492
 
497
- // Update style
493
+ // Add style if present
498
494
  if (resolvedStyle.value) {
499
- reactiveProps.style = resolvedStyle.value
500
- } else {
501
- delete reactiveProps.style
495
+ result.style = resolvedStyle.value
502
496
  }
503
497
 
504
- // Update class
498
+ // Add class if present
505
499
  if (resolvedClassName.value) {
506
- reactiveProps.class = resolvedClassName.value
507
- } else {
508
- delete reactiveProps.class
500
+ result.class = resolvedClassName.value
509
501
  }
510
502
 
511
- // Update disabled props
503
+ // Add disabled props
512
504
  if (options.disabled) {
513
- reactiveProps.role = 'link'
514
- reactiveProps['aria-disabled'] = true
515
- } else {
516
- delete reactiveProps.role
517
- delete reactiveProps['aria-disabled']
505
+ result.role = 'link'
506
+ result['aria-disabled'] = true
518
507
  }
519
508
 
520
- // Update active status
509
+ // Add active status
521
510
  if (isActive.value) {
522
- reactiveProps['data-status'] = 'active'
523
- reactiveProps['aria-current'] = 'page'
524
- } else {
525
- delete reactiveProps['data-status']
526
- delete reactiveProps['aria-current']
511
+ result['data-status'] = 'active'
512
+ result['aria-current'] = 'page'
527
513
  }
528
514
 
529
- // Update transitioning status
515
+ // Add transitioning status
530
516
  if (isTransitioning.value) {
531
- reactiveProps['data-transitioning'] = 'transitioning'
532
- } else {
533
- delete reactiveProps['data-transitioning']
517
+ result['data-transitioning'] = 'transitioning'
534
518
  }
535
519
 
536
520
  // Merge active/inactive props (excluding class and style which are handled above)
521
+ const activeP = resolvedActiveProps.value
522
+ const inactiveP = resolvedInactiveProps.value
523
+
537
524
  for (const key of Object.keys(activeP)) {
538
525
  if (key !== 'class' && key !== 'style') {
539
- reactiveProps[key] = activeP[key]
526
+ result[key] = activeP[key]
540
527
  }
541
528
  }
542
529
  for (const key of Object.keys(inactiveP)) {
543
530
  if (key !== 'class' && key !== 'style') {
544
- reactiveProps[key] = inactiveP[key]
531
+ result[key] = inactiveP[key]
545
532
  }
546
533
  }
534
+
535
+ return result
547
536
  })
548
537
 
549
- return reactiveProps
538
+ // Return the computed ref itself - callers should access .value
539
+ return computedProps as unknown as HTMLAttributes
550
540
  }
551
541
 
552
542
  // Type definitions
@@ -682,13 +672,18 @@ const LinkImpl = Vue.defineComponent({
682
672
  ],
683
673
  setup(props, { attrs, slots }) {
684
674
  // Call useLinkProps ONCE during setup with combined props and attrs
685
- // The returned object includes computed values that update reactively
675
+ // The returned object is a computed ref that updates reactively
686
676
  const allProps = { ...props, ...attrs }
687
- const linkProps = useLinkProps(allProps as any)
677
+ const linkPropsComputed = useLinkProps(
678
+ allProps as any,
679
+ ) as unknown as Vue.ComputedRef<HTMLAttributes>
688
680
 
689
681
  return () => {
690
682
  const Component = props._asChild || 'a'
691
683
 
684
+ // Access the computed value to get fresh props each render
685
+ const linkProps = linkPropsComputed.value
686
+
692
687
  const isActive = linkProps['data-status'] === 'active'
693
688
  const isTransitioning =
694
689
  linkProps['data-transitioning'] === 'transitioning'