Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
28 changes: 17 additions & 11 deletions examples/solid/start-bare/src/routes/__root.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/// <reference types="vite/client" />
import { createRootRoute, Link, Outlet } from '@tanstack/solid-router'
import { createRootRoute, HeadContent, Link, Outlet, Scripts } from '@tanstack/solid-router'
import appCss from '~/styles/app.css?url'
import * as Solid from 'solid-js'
import { TanStackRouterDevtools } from '@tanstack/solid-router-devtools'
import { Hydration, HydrationScript, NoHydration } from 'solid-js/web'

export const Route = createRootRoute({
head: () => ({
Expand All @@ -13,13 +13,19 @@ export const Route = createRootRoute({

function RootDocument({ children }: { children: Solid.JSX.Element }) {
return (
<>
<div>
<Link to="/">Index</Link>
<Link to="/about">About</Link>
</div>
{children}
<TanStackRouterDevtools position="bottom-right" />
</>
<NoHydration>
<html>
<head>
<HeadContent />
<HydrationScript />
</head>
<body>
<Hydration>
{children}
</Hydration>
<Scripts />
</body>
</html>
</NoHydration>
)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify NoHydration wrapping the entire document; nested Hydration may not re-enable hydration.

Placing at the root can disable hydration for descendants. Confirm that inside re-enables hydration for Solid 1.9+. You may need to move to targeted islands or remove it.

Option A (remove NoHydration):

-    <NoHydration>
       <html>
         <head>
           <HeadContent />
           <HydrationScript />
         </head>
         <body>
-          <Hydration>
-            {children}
-          </Hydration>
+          <Hydration>{children}</Hydration>
           <Scripts />
         </body>
-      </html>
-    </NoHydration>
+      </html>

Web check:


🌐 Web query:

In Solid 1.9+, does <Hydration> nested inside <NoHydration> re-enable hydration for its subtree, or does <NoHydration> hard-disable hydration regardless of nesting? Cite official docs/examples.

💡 Result:

No — hard-disables hydration for its subtree. The docs state that placing a inside has no effect and will not override 's behavior. [1] See also the release note describing the API (for starting/stopping hydratable sections) for context. [2]

Sources:
[1] Solid docs — .
[2] Solid release notes (Hydration / Islands support).


Remove wrapper; nested cannot override it.

The web search confirms that hard-disables hydration for its entire subtree— nested inside it has no effect and cannot re-enable hydration. Placing a inside will not override 's behavior. The current code structure prevents hydration from working.

Apply the suggested fix (Option A): remove the outer <NoHydration> wrapper and keep only <Hydration>{children}</Hydration> in the body.

-    <NoHydration>
       <html>
         <head>
           <HeadContent />
           <HydrationScript />
         </head>
         <body>
-          <Hydration>
-            {children}
-          </Hydration>
+          <Hydration>{children}</Hydration>
           <Scripts />
         </body>
-      </html>
-    </NoHydration>
+      </html>
🤖 Prompt for AI Agents
In examples/solid/start-bare/src/routes/__root.tsx around lines 16 to 30, the
root layout wraps the entire document in <NoHydration> which disables hydration
for its subtree and prevents the nested <Hydration> from having any effect;
remove the outer <NoHydration> wrapper and instead wrap only the page children
with <Hydration> in the <body>, leaving <HeadContent /> and <HydrationScript />
in the <head> and keeping <Scripts /> after the hydrated children so hydration
is actually enabled.

}
}
3 changes: 2 additions & 1 deletion packages/solid-router/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@
"@tanstack/solid-store": "0.7.0",
"isbot": "^5.1.22",
"tiny-invariant": "^1.3.3",
"tiny-warning": "^1.0.3"
"tiny-warning": "^1.0.3",
"vite": "^7.1.7"
},
"devDependencies": {
"@solidjs/testing-library": "^0.8.10",
Expand Down
45 changes: 28 additions & 17 deletions packages/solid-router/src/Matches.tsx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// @ts-nocheck
import * as Solid from 'solid-js'
import warning from 'tiny-warning'
import { CatchBoundary, ErrorComponent } from './CatchBoundary'
Expand All @@ -24,6 +25,8 @@ import type {
RouterState,
ToSubOptionsProps,
} from '@tanstack/router-core'
import { Scripts } from './Scripts'
import { HydrationScript } from 'solid-js/web'

declare module '@tanstack/router-core' {
export interface RouteMatchExtensions {
Expand All @@ -38,12 +41,7 @@ declare module '@tanstack/router-core' {
export function Matches() {
const router = useRouter()

// Do not render a root Suspense during SSR or hydrating from SSR
const ResolvedSuspense =
router.isServer || (typeof document !== 'undefined' && router.ssr)
? SafeFragment
: Solid.Suspense

const ResolvedSuspense = Solid.Suspense;
const OptionalWrapper = router.options.InnerWrap || SafeFragment

return (
Expand All @@ -55,7 +53,7 @@ export function Matches() {
) : null
}
>
{!router.isServer && <Transitioner />}
<Transitioner />
<MatchesInner />
</ResolvedSuspense>
</OptionalWrapper>
Expand All @@ -70,13 +68,24 @@ function MatchesInner() {
},
})


const resetKey = useRouterState({
select: (s) => s.loadedAt,
})

const matchComponent = () => {
const id = matchId()
return id ? <Match matchId={id} /> : null
return (
<>
<HydrationScript />

<button onClick={() => {
console.log('click')
}}>
Click me
</button>
<Scripts />
</>
)
}

return (
Expand All @@ -90,7 +99,8 @@ function MatchesInner() {
onCatch={(error) => {
warning(
false,
`The following error wasn't caught by any route! At the very least, consider setting an 'errorComponent' in your RootRoute!`,
`The following error wasn't caught by any route! At the very leas
t, consider setting an 'errorComponent' in your RootRoute!`,
Comment on lines +98 to +99
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Fix malformed error message string.

The error message string is incorrectly split across lines, breaking the word "least" and introducing unwanted whitespace.

Apply this diff to fix the formatting:

             warning(
               false,
-              `The following error wasn't caught by any route! At the very leas
-    t, consider setting an 'errorComponent' in your RootRoute!`,
+              `The following error wasn't caught by any route! At the very least, consider setting an 'errorComponent' in your RootRoute!`,
             )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
`The following error wasn't caught by any route! At the very leas
t, consider setting an 'errorComponent' in your RootRoute!`,
warning(
false,
`The following error wasn't caught by any route! At the very least, consider setting an 'errorComponent' in your RootRoute!`,
)
🤖 Prompt for AI Agents
In packages/solid-router/src/Matches.tsx around lines 102 to 103, the error
message string is split across lines causing the word "least" to be broken and
adding unwanted whitespace; fix it by joining the split string into a single
continuous string (remove the line break and any extra spaces) so the message
reads correctly, e.g. "The following error wasn't caught by any route! At the
very least, consider setting an 'errorComponent' in your RootRoute!".

)
warning(false, error.message || error.toString())
}}
Expand All @@ -102,6 +112,7 @@ function MatchesInner() {
)
}


export type UseMatchRouteOptions<
TRouter extends AnyRouter = RegisteredRouter,
TFrom extends string = string,
Expand Down Expand Up @@ -156,13 +167,13 @@ export type MakeMatchRouteOptions<
> = UseMatchRouteOptions<TRouter, TFrom, TTo, TMaskFrom, TMaskTo> & {
// If a function is passed as a child, it will be given the `isActive` boolean to aid in further styling on the element it returns
children?:
| ((
params?: RouteByPath<
TRouter['routeTree'],
ResolveRelativePath<TFrom, NoInfer<TTo>>
>['types']['allParams'],
) => Solid.JSX.Element)
| Solid.JSX.Element
| ((
params?: RouteByPath<
TRouter['routeTree'],
ResolveRelativePath<TFrom, NoInfer<TTo>>
>['types']['allParams'],
) => Solid.JSX.Element)
| Solid.JSX.Element
}

export function MatchRoute<
Expand Down
40 changes: 9 additions & 31 deletions packages/solid-start-client/src/StartClient.tsx
Original file line number Diff line number Diff line change
@@ -1,37 +1,15 @@
import { Await, HeadContent, RouterProvider } from '@tanstack/solid-router'
import { hydrateStart } from '@tanstack/start-client-core/client'
import { createResource, Suspense } from 'solid-js';
import { Await, RouterProvider } from '@tanstack/solid-router'
import { hydrateStart } from '@tanstack/start-client-core/client';
import type { AnyRouter } from '@tanstack/router-core'
import type { JSXElement } from 'solid-js'

let hydrationPromise: Promise<AnyRouter> | undefined
export function StartClient({ router }: { router: AnyRouter }) {
const [resource] = createResource(() => new Promise(r => r(hydrateStart())))

const Dummy = (props: { children?: JSXElement }) => <>{props.children}</>

export function StartClient() {
if (!hydrationPromise) {
hydrationPromise = hydrateStart()
}
return (
<Await
promise={hydrationPromise}
children={(router) => (
<Dummy>
<Dummy>
<RouterProvider
router={router}
InnerWrap={(props) => (
<Dummy>
<Dummy>
<HeadContent />
{props.children}
</Dummy>
<Dummy />
</Dummy>
)}
/>
</Dummy>
</Dummy>
)}
/>
<Suspense fallback={<div>Loading...</div>}>
<RouterProvider router={router} />
{resource() ? '' : ''}
</Suspense>
)
}
3 changes: 2 additions & 1 deletion packages/solid-start-server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,8 @@
"@tanstack/router-core": "workspace:*",
"@tanstack/solid-router": "workspace:*",
"@tanstack/start-client-core": "workspace:*",
"@tanstack/start-server-core": "workspace:*"
"@tanstack/start-server-core": "workspace:*",
"vite": "^7.1.7"
},
"devDependencies": {
"solid-js": "^1.9.5",
Expand Down
59 changes: 9 additions & 50 deletions packages/solid-start-server/src/StartServer.tsx
Original file line number Diff line number Diff line change
@@ -1,57 +1,16 @@
import { Asset, RouterProvider, Scripts, useTags } from '@tanstack/solid-router'
import {
Hydration,
HydrationScript,
NoHydration,
ssr,
useAssets,
} from 'solid-js/web'
import { MetaProvider } from '@solidjs/meta'
import { RouterProvider } from '@tanstack/solid-router'
import { createResource, Show, Suspense } from 'solid-js'
import type { AnyRouter } from '@tanstack/router-core'

export function ServerHeadContent() {
const tags = useTags()
useAssets(() => {
return (
<MetaProvider>
{tags().map((tag) => (
<Asset {...tag} />
))}
</MetaProvider>
)
})
return null
}

const docType = ssr('<!DOCTYPE html>')

export function StartServer<TRouter extends AnyRouter>(props: {
router: TRouter
}) {
const [resource] = createResource(() => new Promise(r => r(true)))

return (
<NoHydration>
{docType as any}
<html>
<head>
<HydrationScript />
</head>
<body>
<Hydration>
<RouterProvider
router={props.router}
InnerWrap={(props) => (
<NoHydration>
<MetaProvider>
<ServerHeadContent />
<Hydration>{props.children}</Hydration>
<Scripts />
</MetaProvider>
</NoHydration>
)}
/>
</Hydration>
</body>
</html>
</NoHydration>
<Suspense fallback={<div>Loading...</div>}>
<RouterProvider router={props.router} />
{resource() ? '' : ''}
</Suspense>
)
}
}
5 changes: 4 additions & 1 deletion packages/solid-start/src/default-entry/client.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import { hydrate } from 'solid-js/web'
import { StartClient } from '@tanstack/solid-start/client'
import { getRouter } from '#tanstack-router-entry'

hydrate(() => <StartClient />, document.body)
const router = await getRouter();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Add error handling for router initialization.

The top-level await has no try-catch block. If getRouter() rejects, the entire module will fail to execute and hydration will never occur, leaving users with a non-interactive page and no error feedback.

Apply this diff to add error handling:

-const router = await getRouter();
+let router;
+try {
+  router = await getRouter();
+} catch (error) {
+  console.error('Failed to initialize router:', error);
+  throw error; // Re-throw or provide fallback behavior
+}

Alternatively, consider whether router initialization failures should gracefully degrade or display an error boundary to the user.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const router = await getRouter();
let router;
try {
router = await getRouter();
} catch (error) {
console.error('Failed to initialize router:', error);
throw error; // Re-throw or provide fallback behavior
}
🤖 Prompt for AI Agents
In packages/solid-start/src/default-entry/client.tsx around line 5, the
top-level await calling getRouter() has no error handling; wrap the router
initialization in a try-catch so that if getRouter() rejects we catch the error,
log it (or report to your monitoring), and provide a safe fallback (e.g., set
router to null/undefined and proceed with a no-router hydration path or mount an
error boundary/UI message) rather than letting the module throw and block
hydration; ensure any caught error is surfaced appropriately (console/error
logger) and that downstream code checks for a missing router before using it.


hydrate(() => <StartClient router={router} />, document)
Loading