@tanstack/react-router 1.121.0-alpha.22 → 1.121.0-alpha.28

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 (214) hide show
  1. package/dist/cjs/Asset.cjs +83 -16
  2. package/dist/cjs/Asset.cjs.map +1 -1
  3. package/dist/cjs/Asset.d.cts +2 -1
  4. package/dist/cjs/CatchBoundary.cjs.map +1 -1
  5. package/dist/cjs/ClientOnly.cjs.map +1 -1
  6. package/dist/cjs/ClientOnly.d.cts +1 -1
  7. package/dist/cjs/HeadContent.cjs +19 -17
  8. package/dist/cjs/HeadContent.cjs.map +1 -1
  9. package/dist/cjs/Match.cjs +61 -57
  10. package/dist/cjs/Match.cjs.map +1 -1
  11. package/dist/cjs/Matches.cjs +14 -16
  12. package/dist/cjs/Matches.cjs.map +1 -1
  13. package/dist/cjs/Matches.d.cts +2 -2
  14. package/dist/cjs/RouterProvider.cjs.map +1 -1
  15. package/dist/cjs/SafeFragment.cjs.map +1 -1
  16. package/dist/cjs/ScriptOnce.cjs +3 -10
  17. package/dist/cjs/ScriptOnce.cjs.map +1 -1
  18. package/dist/cjs/ScriptOnce.d.cts +1 -1
  19. package/dist/cjs/Scripts.cjs +7 -11
  20. package/dist/cjs/Scripts.cjs.map +1 -1
  21. package/dist/cjs/ScrollRestoration.cjs +3 -4
  22. package/dist/cjs/ScrollRestoration.cjs.map +1 -1
  23. package/dist/cjs/Transitioner.cjs +16 -15
  24. package/dist/cjs/Transitioner.cjs.map +1 -1
  25. package/dist/cjs/awaited.cjs.map +1 -1
  26. package/dist/cjs/fileRoute.cjs +8 -8
  27. package/dist/cjs/fileRoute.cjs.map +1 -1
  28. package/dist/cjs/index.cjs +0 -12
  29. package/dist/cjs/index.cjs.map +1 -1
  30. package/dist/cjs/index.d.cts +4 -8
  31. package/dist/cjs/lazyRouteComponent.cjs +3 -16
  32. package/dist/cjs/lazyRouteComponent.cjs.map +1 -1
  33. package/dist/cjs/lazyRouteComponent.d.cts +1 -1
  34. package/dist/cjs/link.cjs +106 -74
  35. package/dist/cjs/link.cjs.map +1 -1
  36. package/dist/cjs/link.d.cts +1 -5
  37. package/dist/cjs/matchContext.cjs.map +1 -1
  38. package/dist/cjs/not-found.cjs +2 -4
  39. package/dist/cjs/not-found.cjs.map +1 -1
  40. package/dist/cjs/renderRouteNotFound.cjs.map +1 -1
  41. package/dist/cjs/route.cjs +21 -21
  42. package/dist/cjs/route.cjs.map +1 -1
  43. package/dist/cjs/route.d.cts +14 -6
  44. package/dist/cjs/router.cjs.map +1 -1
  45. package/dist/cjs/routerContext.cjs.map +1 -1
  46. package/dist/cjs/scroll-restoration.cjs +9 -3
  47. package/dist/cjs/scroll-restoration.cjs.map +1 -1
  48. package/dist/cjs/ssr/RouterClient.cjs +25 -0
  49. package/dist/cjs/ssr/RouterClient.cjs.map +1 -0
  50. package/dist/cjs/ssr/RouterClient.d.cts +4 -0
  51. package/dist/cjs/ssr/RouterServer.cjs +9 -0
  52. package/dist/cjs/ssr/RouterServer.cjs.map +1 -0
  53. package/dist/cjs/ssr/RouterServer.d.cts +4 -0
  54. package/dist/cjs/ssr/client.cjs +12 -0
  55. package/dist/cjs/ssr/client.cjs.map +1 -0
  56. package/dist/cjs/ssr/client.d.cts +2 -0
  57. package/dist/cjs/ssr/defaultRenderHandler.cjs +15 -0
  58. package/dist/cjs/ssr/defaultRenderHandler.cjs.map +1 -0
  59. package/dist/cjs/ssr/defaultRenderHandler.d.cts +1 -0
  60. package/dist/cjs/ssr/defaultStreamHandler.cjs +16 -0
  61. package/dist/cjs/ssr/defaultStreamHandler.cjs.map +1 -0
  62. package/dist/cjs/ssr/defaultStreamHandler.d.cts +1 -0
  63. package/dist/cjs/ssr/renderRouterToStream.cjs +63 -0
  64. package/dist/cjs/ssr/renderRouterToStream.cjs.map +1 -0
  65. package/dist/cjs/ssr/renderRouterToStream.d.cts +8 -0
  66. package/dist/cjs/ssr/renderRouterToString.cjs +28 -0
  67. package/dist/cjs/ssr/renderRouterToString.cjs.map +1 -0
  68. package/dist/cjs/ssr/renderRouterToString.d.cts +7 -0
  69. package/dist/cjs/ssr/server.cjs +20 -0
  70. package/dist/cjs/ssr/server.cjs.map +1 -0
  71. package/dist/cjs/ssr/server.d.cts +6 -0
  72. package/dist/cjs/useBlocker.cjs.map +1 -1
  73. package/dist/cjs/useCanGoBack.cjs.map +1 -1
  74. package/dist/cjs/useLoaderData.cjs.map +1 -1
  75. package/dist/cjs/useLoaderDeps.cjs.map +1 -1
  76. package/dist/cjs/useLocation.cjs +1 -1
  77. package/dist/cjs/useLocation.cjs.map +1 -1
  78. package/dist/cjs/useMatch.cjs.map +1 -1
  79. package/dist/cjs/useNavigate.cjs +2 -2
  80. package/dist/cjs/useNavigate.cjs.map +1 -1
  81. package/dist/cjs/useParams.cjs.map +1 -1
  82. package/dist/cjs/useRouter.cjs +1 -1
  83. package/dist/cjs/useRouter.cjs.map +1 -1
  84. package/dist/cjs/useRouterState.cjs +3 -3
  85. package/dist/cjs/useRouterState.cjs.map +1 -1
  86. package/dist/cjs/useSearch.cjs.map +1 -1
  87. package/dist/cjs/utils.cjs +4 -10
  88. package/dist/cjs/utils.cjs.map +1 -1
  89. package/dist/cjs/utils.d.cts +1 -1
  90. package/dist/esm/Asset.d.ts +2 -1
  91. package/dist/esm/Asset.js +66 -16
  92. package/dist/esm/Asset.js.map +1 -1
  93. package/dist/esm/CatchBoundary.js.map +1 -1
  94. package/dist/esm/ClientOnly.d.ts +1 -1
  95. package/dist/esm/ClientOnly.js.map +1 -1
  96. package/dist/esm/HeadContent.js +19 -17
  97. package/dist/esm/HeadContent.js.map +1 -1
  98. package/dist/esm/Match.js +63 -59
  99. package/dist/esm/Match.js.map +1 -1
  100. package/dist/esm/Matches.d.ts +2 -2
  101. package/dist/esm/Matches.js +14 -16
  102. package/dist/esm/Matches.js.map +1 -1
  103. package/dist/esm/RouterProvider.js.map +1 -1
  104. package/dist/esm/SafeFragment.js.map +1 -1
  105. package/dist/esm/ScriptOnce.d.ts +1 -1
  106. package/dist/esm/ScriptOnce.js +3 -10
  107. package/dist/esm/ScriptOnce.js.map +1 -1
  108. package/dist/esm/Scripts.js +7 -11
  109. package/dist/esm/Scripts.js.map +1 -1
  110. package/dist/esm/ScrollRestoration.js +3 -4
  111. package/dist/esm/ScrollRestoration.js.map +1 -1
  112. package/dist/esm/Transitioner.js +16 -15
  113. package/dist/esm/Transitioner.js.map +1 -1
  114. package/dist/esm/awaited.js.map +1 -1
  115. package/dist/esm/fileRoute.js +8 -8
  116. package/dist/esm/fileRoute.js.map +1 -1
  117. package/dist/esm/index.d.ts +4 -8
  118. package/dist/esm/index.js +2 -8
  119. package/dist/esm/index.js.map +1 -1
  120. package/dist/esm/lazyRouteComponent.d.ts +1 -1
  121. package/dist/esm/lazyRouteComponent.js +2 -15
  122. package/dist/esm/lazyRouteComponent.js.map +1 -1
  123. package/dist/esm/link.d.ts +1 -5
  124. package/dist/esm/link.js +107 -75
  125. package/dist/esm/link.js.map +1 -1
  126. package/dist/esm/matchContext.js.map +1 -1
  127. package/dist/esm/not-found.js +2 -4
  128. package/dist/esm/not-found.js.map +1 -1
  129. package/dist/esm/renderRouteNotFound.js.map +1 -1
  130. package/dist/esm/route.d.ts +14 -6
  131. package/dist/esm/route.js +21 -21
  132. package/dist/esm/route.js.map +1 -1
  133. package/dist/esm/router.js.map +1 -1
  134. package/dist/esm/routerContext.js.map +1 -1
  135. package/dist/esm/scroll-restoration.js +9 -3
  136. package/dist/esm/scroll-restoration.js.map +1 -1
  137. package/dist/esm/ssr/RouterClient.d.ts +4 -0
  138. package/dist/esm/ssr/RouterClient.js +25 -0
  139. package/dist/esm/ssr/RouterClient.js.map +1 -0
  140. package/dist/esm/ssr/RouterServer.d.ts +4 -0
  141. package/dist/esm/ssr/RouterServer.js +9 -0
  142. package/dist/esm/ssr/RouterServer.js.map +1 -0
  143. package/dist/esm/ssr/client.d.ts +2 -0
  144. package/dist/esm/ssr/client.js +6 -0
  145. package/dist/esm/ssr/client.js.map +1 -0
  146. package/dist/esm/ssr/defaultRenderHandler.d.ts +1 -0
  147. package/dist/esm/ssr/defaultRenderHandler.js +15 -0
  148. package/dist/esm/ssr/defaultRenderHandler.js.map +1 -0
  149. package/dist/esm/ssr/defaultStreamHandler.d.ts +1 -0
  150. package/dist/esm/ssr/defaultStreamHandler.js +16 -0
  151. package/dist/esm/ssr/defaultStreamHandler.js.map +1 -0
  152. package/dist/esm/ssr/renderRouterToStream.d.ts +8 -0
  153. package/dist/esm/ssr/renderRouterToStream.js +63 -0
  154. package/dist/esm/ssr/renderRouterToStream.js.map +1 -0
  155. package/dist/esm/ssr/renderRouterToString.d.ts +7 -0
  156. package/dist/esm/ssr/renderRouterToString.js +28 -0
  157. package/dist/esm/ssr/renderRouterToString.js.map +1 -0
  158. package/dist/esm/ssr/server.d.ts +6 -0
  159. package/dist/esm/ssr/server.js +14 -0
  160. package/dist/esm/ssr/server.js.map +1 -0
  161. package/dist/esm/useBlocker.js.map +1 -1
  162. package/dist/esm/useCanGoBack.js.map +1 -1
  163. package/dist/esm/useLoaderData.js.map +1 -1
  164. package/dist/esm/useLoaderDeps.js.map +1 -1
  165. package/dist/esm/useLocation.js +1 -1
  166. package/dist/esm/useLocation.js.map +1 -1
  167. package/dist/esm/useMatch.js.map +1 -1
  168. package/dist/esm/useNavigate.js +2 -2
  169. package/dist/esm/useNavigate.js.map +1 -1
  170. package/dist/esm/useParams.js.map +1 -1
  171. package/dist/esm/useRouter.js +1 -1
  172. package/dist/esm/useRouter.js.map +1 -1
  173. package/dist/esm/useRouterState.js +3 -3
  174. package/dist/esm/useRouterState.js.map +1 -1
  175. package/dist/esm/useSearch.js.map +1 -1
  176. package/dist/esm/utils.d.ts +1 -1
  177. package/dist/esm/utils.js +4 -10
  178. package/dist/esm/utils.js.map +1 -1
  179. package/dist/llms/index.d.ts +3 -0
  180. package/dist/llms/index.js +35 -0
  181. package/dist/llms/rules/api.d.ts +2 -0
  182. package/dist/llms/rules/api.js +4326 -0
  183. package/dist/llms/rules/guide.d.ts +2 -0
  184. package/dist/llms/rules/guide.js +7096 -0
  185. package/dist/llms/rules/routing.d.ts +2 -0
  186. package/dist/llms/rules/routing.js +1981 -0
  187. package/dist/llms/rules/setup-and-architecture.d.ts +2 -0
  188. package/dist/llms/rules/setup-and-architecture.js +945 -0
  189. package/package.json +32 -6
  190. package/src/Asset.tsx +95 -16
  191. package/src/ClientOnly.tsx +1 -1
  192. package/src/HeadContent.tsx +16 -0
  193. package/src/Match.tsx +86 -63
  194. package/src/Matches.tsx +24 -17
  195. package/src/ScriptOnce.tsx +2 -14
  196. package/src/Transitioner.tsx +13 -14
  197. package/src/index.tsx +3 -21
  198. package/src/lazyRouteComponent.tsx +6 -31
  199. package/src/link.tsx +130 -99
  200. package/src/not-found.tsx +1 -1
  201. package/src/route.tsx +18 -9
  202. package/src/scroll-restoration.tsx +10 -3
  203. package/src/ssr/RouterClient.tsx +22 -0
  204. package/src/ssr/RouterServer.tsx +9 -0
  205. package/src/ssr/client.ts +2 -0
  206. package/src/ssr/defaultRenderHandler.tsx +12 -0
  207. package/src/ssr/defaultStreamHandler.tsx +13 -0
  208. package/src/ssr/renderRouterToStream.tsx +79 -0
  209. package/src/ssr/renderRouterToString.tsx +31 -0
  210. package/src/ssr/server.ts +6 -0
  211. package/src/utils.ts +6 -14
  212. package/dist/cjs/serializer.d.cts +0 -6
  213. package/dist/esm/serializer.d.ts +0 -6
  214. package/src/serializer.ts +0 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tanstack/react-router",
3
- "version": "1.121.0-alpha.22",
3
+ "version": "1.121.0-alpha.28",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -38,7 +38,33 @@
38
38
  "default": "./dist/cjs/index.cjs"
39
39
  }
40
40
  },
41
- "./package.json": "./package.json"
41
+ "./ssr/server": {
42
+ "import": {
43
+ "types": "./dist/esm/ssr/server.d.ts",
44
+ "default": "./dist/esm/ssr/server.js"
45
+ },
46
+ "require": {
47
+ "types": "./dist/cjs/ssr/server.d.cts",
48
+ "default": "./dist/cjs/ssr/server.cjs"
49
+ }
50
+ },
51
+ "./ssr/client": {
52
+ "import": {
53
+ "types": "./dist/esm/ssr/client.d.ts",
54
+ "default": "./dist/esm/ssr/client.js"
55
+ },
56
+ "require": {
57
+ "types": "./dist/cjs/ssr/client.d.cts",
58
+ "default": "./dist/cjs/ssr/client.cjs"
59
+ }
60
+ },
61
+ "./package.json": "./package.json",
62
+ "./llms": {
63
+ "import": {
64
+ "types": "./dist/llms/index.d.ts",
65
+ "default": "./dist/llms/index.js"
66
+ }
67
+ }
42
68
  },
43
69
  "sideEffects": false,
44
70
  "files": [
@@ -50,20 +76,20 @@
50
76
  },
51
77
  "dependencies": {
52
78
  "@tanstack/react-store": "^0.7.0",
53
- "jsesc": "^3.1.0",
79
+ "isbot": "^5.1.22",
54
80
  "tiny-invariant": "^1.3.3",
55
81
  "tiny-warning": "^1.0.3",
56
- "@tanstack/history": "1.121.0-alpha.1",
57
- "@tanstack/router-core": "1.121.0-alpha.22"
82
+ "@tanstack/history": "1.121.0-alpha.28",
83
+ "@tanstack/router-core": "1.121.0-alpha.28"
58
84
  },
59
85
  "devDependencies": {
60
86
  "@testing-library/jest-dom": "^6.6.3",
61
87
  "@testing-library/react": "^16.2.0",
62
- "@types/jsesc": "^3.0.3",
63
88
  "@vitejs/plugin-react": "^4.3.4",
64
89
  "combinate": "^1.1.11",
65
90
  "react": "^19.0.0",
66
91
  "react-dom": "^19.0.0",
92
+ "vibe-rules": "^0.2.57",
67
93
  "zod": "^3.24.2"
68
94
  },
69
95
  "peerDependencies": {
package/src/Asset.tsx CHANGED
@@ -1,6 +1,17 @@
1
+ import * as React from 'react'
1
2
  import type { RouterManagedTag } from '@tanstack/router-core'
2
3
 
3
- export function Asset({ tag, attrs, children }: RouterManagedTag): any {
4
+ interface ScriptAttrs {
5
+ [key: string]: string | boolean | undefined
6
+ src?: string
7
+ suppressHydrationWarning?: boolean
8
+ }
9
+
10
+ export function Asset({
11
+ tag,
12
+ attrs,
13
+ children,
14
+ }: RouterManagedTag): React.ReactElement | null {
4
15
  switch (tag) {
5
16
  case 'title':
6
17
  return (
@@ -16,25 +27,93 @@ export function Asset({ tag, attrs, children }: RouterManagedTag): any {
16
27
  return (
17
28
  <style
18
29
  {...attrs}
19
- dangerouslySetInnerHTML={{ __html: children as any }}
30
+ dangerouslySetInnerHTML={{ __html: children as string }}
20
31
  />
21
32
  )
22
33
  case 'script':
23
- if ((attrs as any) && (attrs as any).src) {
24
- return <script {...attrs} suppressHydrationWarning />
25
- }
26
- if (typeof children === 'string')
27
- return (
28
- <script
29
- {...attrs}
30
- dangerouslySetInnerHTML={{
31
- __html: children,
32
- }}
33
- suppressHydrationWarning
34
- />
35
- )
36
- return null
34
+ return <Script attrs={attrs}>{children}</Script>
37
35
  default:
38
36
  return null
39
37
  }
40
38
  }
39
+
40
+ function Script({
41
+ attrs,
42
+ children,
43
+ }: {
44
+ attrs?: ScriptAttrs
45
+ children?: string
46
+ }) {
47
+ React.useEffect(() => {
48
+ if (attrs?.src) {
49
+ const script = document.createElement('script')
50
+
51
+ for (const [key, value] of Object.entries(attrs)) {
52
+ if (
53
+ key !== 'suppressHydrationWarning' &&
54
+ value !== undefined &&
55
+ value !== false
56
+ ) {
57
+ script.setAttribute(
58
+ key,
59
+ typeof value === 'boolean' ? '' : String(value),
60
+ )
61
+ }
62
+ }
63
+
64
+ document.head.appendChild(script)
65
+
66
+ return () => {
67
+ if (script.parentNode) {
68
+ script.parentNode.removeChild(script)
69
+ }
70
+ }
71
+ }
72
+
73
+ if (typeof children === 'string') {
74
+ const script = document.createElement('script')
75
+ script.textContent = children
76
+
77
+ if (attrs) {
78
+ for (const [key, value] of Object.entries(attrs)) {
79
+ if (
80
+ key !== 'suppressHydrationWarning' &&
81
+ value !== undefined &&
82
+ value !== false
83
+ ) {
84
+ script.setAttribute(
85
+ key,
86
+ typeof value === 'boolean' ? '' : String(value),
87
+ )
88
+ }
89
+ }
90
+ }
91
+
92
+ document.head.appendChild(script)
93
+
94
+ return () => {
95
+ if (script.parentNode) {
96
+ script.parentNode.removeChild(script)
97
+ }
98
+ }
99
+ }
100
+
101
+ return undefined
102
+ }, [attrs, children])
103
+
104
+ if (attrs?.src && typeof attrs.src === 'string') {
105
+ return <script {...attrs} suppressHydrationWarning />
106
+ }
107
+
108
+ if (typeof children === 'string') {
109
+ return (
110
+ <script
111
+ {...attrs}
112
+ dangerouslySetInnerHTML={{ __html: children }}
113
+ suppressHydrationWarning
114
+ />
115
+ )
116
+ }
117
+
118
+ return null
119
+ }
@@ -2,7 +2,7 @@ import React from 'react'
2
2
 
3
3
  export interface ClientOnlyProps {
4
4
  /**
5
- * The children to render if the JS is loaded.
5
+ * The children to render when the JS is loaded.
6
6
  */
7
7
  children: React.ReactNode
8
8
  /**
@@ -120,6 +120,21 @@ export const useTags = () => {
120
120
  structuralSharing: true as any,
121
121
  })
122
122
 
123
+ const styles = useRouterState({
124
+ select: (state) =>
125
+ (
126
+ state.matches
127
+ .map((match) => match.styles!)
128
+ .flat(1)
129
+ .filter(Boolean) as Array<RouterManagedTag>
130
+ ).map(({ children, ...attrs }) => ({
131
+ tag: 'style',
132
+ attrs,
133
+ children,
134
+ })),
135
+ structuralSharing: true as any,
136
+ })
137
+
123
138
  const headScripts = useRouterState({
124
139
  select: (state) =>
125
140
  (
@@ -142,6 +157,7 @@ export const useTags = () => {
142
157
  ...meta,
143
158
  ...preloadMeta,
144
159
  ...links,
160
+ ...styles,
145
161
  ...headScripts,
146
162
  ] as Array<RouterManagedTag>,
147
163
  (d) => {
package/src/Match.tsx CHANGED
@@ -17,7 +17,12 @@ import { matchContext } from './matchContext'
17
17
  import { SafeFragment } from './SafeFragment'
18
18
  import { renderRouteNotFound } from './renderRouteNotFound'
19
19
  import { ScrollRestoration } from './scroll-restoration'
20
- import type { AnyRoute, ParsedLocation } from '@tanstack/router-core'
20
+ import { ClientOnly } from './ClientOnly'
21
+ import type {
22
+ AnyRoute,
23
+ ParsedLocation,
24
+ RootRouteOptions,
25
+ } from '@tanstack/router-core'
21
26
 
22
27
  export const Match = React.memo(function MatchImpl({
23
28
  matchId,
@@ -25,16 +30,19 @@ export const Match = React.memo(function MatchImpl({
25
30
  matchId: string
26
31
  }) {
27
32
  const router = useRouter()
28
- const routeId = useRouterState({
29
- select: (s) => s.matches.find((d) => d.id === matchId)?.routeId as string,
33
+ const matchState = useRouterState({
34
+ select: (s) => {
35
+ const match = s.matches.find((d) => d.id === matchId)
36
+ invariant(
37
+ match,
38
+ `Could not find match for matchId "${matchId}". Please file an issue!`,
39
+ )
40
+ return pick(match, ['routeId', 'ssr', '_displayPending'])
41
+ },
42
+ structuralSharing: true as any,
30
43
  })
31
44
 
32
- invariant(
33
- routeId,
34
- `Could not find routeId for matchId "${matchId}". Please file an issue!`,
35
- )
36
-
37
- const route: AnyRoute = router.routesById[routeId]
45
+ const route: AnyRoute = router.routesById[matchState.routeId]
38
46
 
39
47
  const PendingComponent =
40
48
  route.options.pendingComponent ?? router.options.defaultPendingComponent
@@ -52,12 +60,14 @@ export const Match = React.memo(function MatchImpl({
52
60
  router.options.notFoundRoute?.options.component)
53
61
  : route.options.notFoundComponent
54
62
 
63
+ const resolvedNoSsr =
64
+ matchState.ssr === false || matchState.ssr === 'data-only'
55
65
  const ResolvedSuspenseBoundary =
56
66
  // If we're on the root route, allow forcefully wrapping in suspense
57
- (!route.isRoot || route.options.wrapInSuspense) &&
67
+ (!route.isRoot || route.options.wrapInSuspense || resolvedNoSsr) &&
58
68
  (route.options.wrapInSuspense ??
59
69
  PendingComponent ??
60
- (route.options.errorComponent as any)?.preload)
70
+ ((route.options.errorComponent as any)?.preload || resolvedNoSsr))
61
71
  ? React.Suspense
62
72
  : SafeFragment
63
73
 
@@ -80,8 +90,11 @@ export const Match = React.memo(function MatchImpl({
80
90
  },
81
91
  })
82
92
 
93
+ const ShellComponent = route.isRoot
94
+ ? ((route.options as RootRouteOptions).shellComponent ?? SafeFragment)
95
+ : SafeFragment
83
96
  return (
84
- <>
97
+ <ShellComponent>
85
98
  <matchContext.Provider value={matchId}>
86
99
  <ResolvedSuspenseBoundary fallback={pendingElement}>
87
100
  <ResolvedCatchBoundary
@@ -100,7 +113,7 @@ export const Match = React.memo(function MatchImpl({
100
113
  // route ID which doesn't match the current route, rethrow the error
101
114
  if (
102
115
  !routeNotFoundComponent ||
103
- (error.routeId && error.routeId !== routeId) ||
116
+ (error.routeId && error.routeId !== matchState.routeId) ||
104
117
  (!error.routeId && !route.isRoot)
105
118
  )
106
119
  throw error
@@ -108,7 +121,13 @@ export const Match = React.memo(function MatchImpl({
108
121
  return React.createElement(routeNotFoundComponent, error as any)
109
122
  }}
110
123
  >
111
- <MatchInner matchId={matchId} />
124
+ {resolvedNoSsr || matchState._displayPending ? (
125
+ <ClientOnly fallback={pendingElement}>
126
+ <MatchInner matchId={matchId} />
127
+ </ClientOnly>
128
+ ) : (
129
+ <MatchInner matchId={matchId} />
130
+ )}
112
131
  </ResolvedNotFoundBoundary>
113
132
  </ResolvedCatchBoundary>
114
133
  </ResolvedSuspenseBoundary>
@@ -119,14 +138,14 @@ export const Match = React.memo(function MatchImpl({
119
138
  <ScrollRestoration />
120
139
  </>
121
140
  ) : null}
122
- </>
141
+ </ShellComponent>
123
142
  )
124
143
  })
125
144
 
126
145
  // On Rendered can't happen above the root layout because it actually
127
146
  // renders a dummy dom element to track the rendered state of the app.
128
147
  // We render a script tag with a key that changes based on the current
129
- // location state.key. Also, because it's below the root layout, it
148
+ // location state.__TSR_key. Also, because it's below the root layout, it
130
149
  // allows us to fire onRendered events even after a hydration mismatch
131
150
  // error that occurred above the root layout (like bad head/link tags,
132
151
  // which is common).
@@ -139,7 +158,7 @@ function OnRendered() {
139
158
 
140
159
  return (
141
160
  <script
142
- key={router.latestLocation.state.key}
161
+ key={router.latestLocation.state.__TSR_key}
143
162
  suppressHydrationWarning
144
163
  ref={(el) => {
145
164
  if (
@@ -185,7 +204,13 @@ export const MatchInner = React.memo(function MatchInnerImpl({
185
204
  return {
186
205
  key,
187
206
  routeId,
188
- match: pick(match, ['id', 'status', 'error']),
207
+ match: pick(match, [
208
+ 'id',
209
+ 'status',
210
+ 'error',
211
+ '_forcePending',
212
+ '_displayPending',
213
+ ]),
189
214
  }
190
215
  },
191
216
  structuralSharing: true as any,
@@ -201,9 +226,45 @@ export const MatchInner = React.memo(function MatchInnerImpl({
201
226
  return <Outlet />
202
227
  }, [key, route.options.component, router.options.defaultComponent])
203
228
 
204
- const RouteErrorComponent =
205
- (route.options.errorComponent ?? router.options.defaultErrorComponent) ||
206
- ErrorComponent
229
+ if (match._displayPending) {
230
+ throw router.getMatch(match.id)?.displayPendingPromise
231
+ }
232
+
233
+ if (match._forcePending) {
234
+ throw router.getMatch(match.id)?.minPendingPromise
235
+ }
236
+
237
+ // see also hydrate() in packages/router-core/src/ssr/ssr-client.ts
238
+ if (match.status === 'pending') {
239
+ // We're pending, and if we have a minPendingMs, we need to wait for it
240
+ const pendingMinMs =
241
+ route.options.pendingMinMs ?? router.options.defaultPendingMinMs
242
+
243
+ if (pendingMinMs && !router.getMatch(match.id)?.minPendingPromise) {
244
+ // Create a promise that will resolve after the minPendingMs
245
+ if (!router.isServer) {
246
+ const minPendingPromise = createControlledPromise<void>()
247
+
248
+ Promise.resolve().then(() => {
249
+ router.updateMatch(match.id, (prev) => ({
250
+ ...prev,
251
+ minPendingPromise,
252
+ }))
253
+ })
254
+
255
+ setTimeout(() => {
256
+ minPendingPromise.resolve()
257
+
258
+ // We've handled the minPendingPromise, so we can delete it
259
+ router.updateMatch(match.id, (prev) => ({
260
+ ...prev,
261
+ minPendingPromise: undefined,
262
+ }))
263
+ }, pendingMinMs)
264
+ }
265
+ }
266
+ throw router.getMatch(match.id)?.loadPromise
267
+ }
207
268
 
208
269
  if (match.status === 'notFound') {
209
270
  invariant(isNotFound(match.error), 'Expected a notFound error')
@@ -229,6 +290,10 @@ export const MatchInner = React.memo(function MatchInnerImpl({
229
290
  // renderToPipeableStream to not hang indefinitely.
230
291
  // We'll serialize the error and rethrow it on the client.
231
292
  if (router.isServer) {
293
+ const RouteErrorComponent =
294
+ (route.options.errorComponent ??
295
+ router.options.defaultErrorComponent) ||
296
+ ErrorComponent
232
297
  return (
233
298
  <RouteErrorComponent
234
299
  error={match.error as any}
@@ -243,37 +308,6 @@ export const MatchInner = React.memo(function MatchInnerImpl({
243
308
  throw match.error
244
309
  }
245
310
 
246
- if (match.status === 'pending') {
247
- // We're pending, and if we have a minPendingMs, we need to wait for it
248
- const pendingMinMs =
249
- route.options.pendingMinMs ?? router.options.defaultPendingMinMs
250
-
251
- if (pendingMinMs && !router.getMatch(match.id)?.minPendingPromise) {
252
- // Create a promise that will resolve after the minPendingMs
253
- if (!router.isServer) {
254
- const minPendingPromise = createControlledPromise<void>()
255
-
256
- Promise.resolve().then(() => {
257
- router.updateMatch(match.id, (prev) => ({
258
- ...prev,
259
- minPendingPromise,
260
- }))
261
- })
262
-
263
- setTimeout(() => {
264
- minPendingPromise.resolve()
265
-
266
- // We've handled the minPendingPromise, so we can delete it
267
- router.updateMatch(match.id, (prev) => ({
268
- ...prev,
269
- minPendingPromise: undefined,
270
- }))
271
- }, pendingMinMs)
272
- }
273
- }
274
- throw router.getMatch(match.id)?.loadPromise
275
- }
276
-
277
311
  return out
278
312
  })
279
313
 
@@ -310,13 +344,6 @@ export const Outlet = React.memo(function OutletImpl() {
310
344
  <router.options.defaultPendingComponent />
311
345
  ) : null
312
346
 
313
- if (router.isShell)
314
- return (
315
- <React.Suspense fallback={pendingElement}>
316
- <ShellInner />
317
- </React.Suspense>
318
- )
319
-
320
347
  if (parentGlobalNotFound) {
321
348
  return renderRouteNotFound(router, route, undefined)
322
349
  }
@@ -335,7 +362,3 @@ export const Outlet = React.memo(function OutletImpl() {
335
362
 
336
363
  return nextMatch
337
364
  })
338
-
339
- function ShellInner(): React.ReactElement {
340
- throw new Error('ShellBoundaryError')
341
- }
package/src/Matches.tsx CHANGED
@@ -11,7 +11,6 @@ import type {
11
11
  StructuralSharingOption,
12
12
  ValidateSelected,
13
13
  } from './structuralSharing'
14
- import type { ReactNode } from './route'
15
14
  import type {
16
15
  AnyRouter,
17
16
  DeepPartial,
@@ -35,6 +34,7 @@ declare module '@tanstack/router-core' {
35
34
  meta?: Array<React.JSX.IntrinsicElements['meta'] | undefined>
36
35
  links?: Array<React.JSX.IntrinsicElements['link'] | undefined>
37
36
  scripts?: Array<React.JSX.IntrinsicElements['script'] | undefined>
37
+ styles?: Array<React.JSX.IntrinsicElements['style'] | undefined>
38
38
  headScripts?: Array<React.JSX.IntrinsicElements['script'] | undefined>
39
39
  }
40
40
  }
@@ -48,13 +48,13 @@ export function Matches() {
48
48
 
49
49
  // Do not render a root Suspense during SSR or hydrating from SSR
50
50
  const ResolvedSuspense =
51
- router.isServer || (typeof document !== 'undefined' && router.clientSsr)
51
+ router.isServer || (typeof document !== 'undefined' && router.ssr)
52
52
  ? SafeFragment
53
53
  : React.Suspense
54
54
 
55
55
  const inner = (
56
56
  <ResolvedSuspense fallback={pendingElement}>
57
- <Transitioner />
57
+ {!router.isServer && <Transitioner />}
58
58
  <MatchesInner />
59
59
  </ResolvedSuspense>
60
60
  )
@@ -67,6 +67,7 @@ export function Matches() {
67
67
  }
68
68
 
69
69
  function MatchesInner() {
70
+ const router = useRouter()
70
71
  const matchId = useRouterState({
71
72
  select: (s) => {
72
73
  return s.matches[0]?.id
@@ -77,21 +78,27 @@ function MatchesInner() {
77
78
  select: (s) => s.loadedAt,
78
79
  })
79
80
 
81
+ const matchComponent = matchId ? <Match matchId={matchId} /> : null
82
+
80
83
  return (
81
84
  <matchContext.Provider value={matchId}>
82
- <CatchBoundary
83
- getResetKey={() => resetKey}
84
- errorComponent={ErrorComponent}
85
- onCatch={(error) => {
86
- warning(
87
- false,
88
- `The following error wasn't caught by any route! At the very least, consider setting an 'errorComponent' in your RootRoute!`,
89
- )
90
- warning(false, error.message || error.toString())
91
- }}
92
- >
93
- {matchId ? <Match matchId={matchId} /> : null}
94
- </CatchBoundary>
85
+ {router.options.disableGlobalCatchBoundary ? (
86
+ matchComponent
87
+ ) : (
88
+ <CatchBoundary
89
+ getResetKey={() => resetKey}
90
+ errorComponent={ErrorComponent}
91
+ onCatch={(error) => {
92
+ warning(
93
+ false,
94
+ `The following error wasn't caught by any route! At the very least, consider setting an 'errorComponent' in your RootRoute!`,
95
+ )
96
+ warning(false, error.message || error.toString())
97
+ }}
98
+ >
99
+ {matchComponent}
100
+ </CatchBoundary>
101
+ )}
95
102
  </matchContext.Provider>
96
103
  )
97
104
  }
@@ -154,7 +161,7 @@ export type MakeMatchRouteOptions<
154
161
  TRouter['routeTree'],
155
162
  ResolveRelativePath<TFrom, NoInfer<TTo>>
156
163
  >['types']['allParams'],
157
- ) => ReactNode)
164
+ ) => React.ReactNode)
158
165
  | React.ReactNode
159
166
  }
160
167
 
@@ -1,8 +1,5 @@
1
- import jsesc from 'jsesc'
2
-
3
1
  export function ScriptOnce({
4
2
  children,
5
- log,
6
3
  }: {
7
4
  children: string
8
5
  log?: boolean
@@ -14,18 +11,9 @@ export function ScriptOnce({
14
11
 
15
12
  return (
16
13
  <script
17
- className="tsr-once"
14
+ className="$tsr"
18
15
  dangerouslySetInnerHTML={{
19
- __html: [
20
- children,
21
- (log ?? true) && process.env.NODE_ENV === 'development'
22
- ? `console.info(\`Injected From Server:
23
- ${jsesc(children.toString(), { quotes: 'backtick' })}\`)`
24
- : '',
25
- 'if (typeof __TSR_SSR__ !== "undefined") __TSR_SSR__.cleanScripts()',
26
- ]
27
- .filter(Boolean)
28
- .join('\n'),
16
+ __html: [children].filter(Boolean).join('\n'),
29
17
  }}
30
18
  />
31
19
  )
@@ -11,14 +11,14 @@ import { useRouterState } from './useRouterState'
11
11
  export function Transitioner() {
12
12
  const router = useRouter()
13
13
  const mountLoadForRouter = React.useRef({ router, mounted: false })
14
- const isLoading = useRouterState({
15
- select: ({ isLoading }) => isLoading,
16
- })
17
14
 
18
15
  const [isTransitioning, setIsTransitioning] = React.useState(false)
19
16
  // Track pending state changes
20
- const hasPendingMatches = useRouterState({
21
- select: (s) => s.matches.some((d) => d.status === 'pending'),
17
+ const { hasPendingMatches, isLoading } = useRouterState({
18
+ select: (s) => ({
19
+ isLoading: s.isLoading,
20
+ hasPendingMatches: s.matches.some((d) => d.status === 'pending'),
21
+ }),
22
22
  structuralSharing: true,
23
23
  })
24
24
 
@@ -30,14 +30,12 @@ export function Transitioner() {
30
30
  const isPagePending = isLoading || hasPendingMatches
31
31
  const previousIsPagePending = usePrevious(isPagePending)
32
32
 
33
- if (!router.isServer) {
34
- router.startTransition = (fn: () => void) => {
35
- setIsTransitioning(true)
36
- React.startTransition(() => {
37
- fn()
38
- setIsTransitioning(false)
39
- })
40
- }
33
+ router.startTransition = (fn: () => void) => {
34
+ setIsTransitioning(true)
35
+ React.startTransition(() => {
36
+ fn()
37
+ setIsTransitioning(false)
38
+ })
41
39
  }
42
40
 
43
41
  // Subscribe to location changes
@@ -69,7 +67,8 @@ export function Transitioner() {
69
67
  // Try to load the initial location
70
68
  useLayoutEffect(() => {
71
69
  if (
72
- (typeof window !== 'undefined' && router.clientSsr) ||
70
+ // if we are hydrating from SSR, loading is triggered in ssr-client
71
+ (typeof window !== 'undefined' && router.ssr) ||
73
72
  (mountLoadForRouter.current.router === router &&
74
73
  mountLoadForRouter.current.mounted)
75
74
  ) {