-
-
Notifications
You must be signed in to change notification settings - Fork 1.3k
Solid hydration #5518
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Solid hydration #5518
Changes from 3 commits
77661ce
3335060
9a6e548
78de124
d5b6e96
326993a
7c54729
69f4878
7c2df63
5354deb
2aed1d8
de21f0b
509fa62
32c660f
fe41bed
489bfde
0968563
fe255da
bbce1fb
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,3 +1,4 @@ | ||||||||||||||
| // @ts-nocheck | ||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||||||||||||||
| import * as Solid from 'solid-js' | ||||||||||||||
| import warning from 'tiny-warning' | ||||||||||||||
| import { CatchBoundary, ErrorComponent } from './CatchBoundary' | ||||||||||||||
|
|
@@ -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 { | ||||||||||||||
|
|
@@ -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 ( | ||||||||||||||
|
|
@@ -55,7 +53,7 @@ export function Matches() { | |||||||||||||
| ) : null | ||||||||||||||
| } | ||||||||||||||
| > | ||||||||||||||
| {!router.isServer && <Transitioner />} | ||||||||||||||
| <Transitioner /> | ||||||||||||||
| <MatchesInner /> | ||||||||||||||
| </ResolvedSuspense> | ||||||||||||||
| </OptionalWrapper> | ||||||||||||||
|
|
@@ -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 /> | ||||||||||||||
| </> | ||||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
coderabbitai[bot] marked this conversation as resolved.
Show resolved
Hide resolved
|
||||||||||||||
|
|
||||||||||||||
| return ( | ||||||||||||||
|
|
@@ -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
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 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
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||
| ) | ||||||||||||||
| warning(false, error.message || error.toString()) | ||||||||||||||
| }} | ||||||||||||||
|
|
@@ -102,6 +112,7 @@ function MatchesInner() { | |||||||||||||
| ) | ||||||||||||||
| } | ||||||||||||||
|
|
||||||||||||||
|
|
||||||||||||||
| export type UseMatchRouteOptions< | ||||||||||||||
| TRouter extends AnyRouter = RegisteredRouter, | ||||||||||||||
| TFrom extends string = string, | ||||||||||||||
|
|
@@ -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< | ||||||||||||||
|
|
||||||||||||||
| 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'; | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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> | ||
| ) | ||
| } | ||
| 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' | ||
coderabbitai[bot] marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| 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> | ||
| ) | ||
| } | ||
| } | ||
| 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(); | ||||||||||||||||||
|
||||||||||||||||||
| 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.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 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):
Web check:
🌐 Web query:
💡 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.🤖 Prompt for AI Agents