createEffect and onMount don't fire on hydrated routes (Solid Start + TanStack Solid Router)
Summary
For routes rendered via Solid Start (SSR + client hydration), Solid lifecycle hooks (createEffect, onMount) never fire on the client, even though:
- The route component body does execute on the client (signals can be created)
onCleanup does fire (so a Solid scope is being created and then disposed)
- Plain JS scheduling (
setTimeout, queueMicrotask, requestAnimationFrame, Promise.then) all fire normally
- Event handlers attached via JSX work (the hydrated DOM is interactive)
Routes with ssr: false are unaffected — they go through a fresh-render path on the client and lifecycle hooks work as expected.
Versions
@tanstack/solid-start: ^1.168.13
@tanstack/solid-router: ^1.170.8
@tanstack/solid-router-ssr-query: ^1.167.0
@tanstack/router-plugin: ^1.168.11
solid-js: ^1.9.13
vite: ^8.0.0
Minimal reproduction
Any default-SSR route:
// src/routes/index.tsx
import { createFileRoute } from '@tanstack/solid-router'
import { createEffect, createSignal, onCleanup, onMount } from 'solid-js'
export const Route = createFileRoute('/')({ component: Home })
function Home() {
const tag = `[diag] ${typeof window === 'undefined' ? 'SSR' : 'CLIENT'}`
console.log(`${tag} 1. body`)
const [count] = createSignal(0)
console.log(`${tag} 2. createSignal returned`, count())
createEffect(() => {
console.log(`${tag} 3. createEffect`)
})
onMount(() => {
console.log(`${tag} 4. onMount`)
})
onCleanup(() => {
console.log(`${tag} 5. onCleanup`)
})
if (typeof window !== 'undefined') {
queueMicrotask(() => console.log(`${tag} 6. queueMicrotask`))
setTimeout(() => console.log(`${tag} 7. setTimeout`), 0)
requestAnimationFrame(() => console.log(`${tag} 8. rAF`))
}
return <h1>hello</h1>
}
Observed client console output
[diag] CLIENT 1. body
[diag] CLIENT 2. createSignal returned 0
[diag] CLIENT 6. queueMicrotask
[diag] CLIENT 7. setTimeout
[diag] CLIENT 8. rAF
[diag] CLIENT 5. onCleanup
Missing: 3. createEffect and 4. onMount.
Expected
createEffect and onMount should fire on the client during/after hydration, as they do for non-routed Solid components and for ssr: false routes.
Interpretation
onCleanup firing strongly suggests the route component is called inside a disposable scope (probably as part of route resolution / dry-run for matching) that is torn down before reactive effects can flush. The actual interactive DOM is then hydrated from SSR'd HTML in a separate scope where the function isn't re-invoked, so lifecycle hooks scheduled by the first invocation never run.
If that's the intended design, it should probably be documented prominently (since onMount-for-client-side-init is a near-universal Solid pattern). If it's not intended, the route component's reactive scope needs to be allowed to flush effects before disposal.
Workarounds (currently in use)
For SSR'd routes:
- Render-body side effect with
!isServer guard — works for idempotent operations (writing to documentElement, preloading images, etc.).
ssr: false on the route — gives up SSR for the route but lifecycle hooks then work normally.
setTimeout(() => ..., 0) from the render body — defers past the disposed scope, fires reliably.
Cross-route observation
In our app, /account/children (which has ssr: false) uses createQuery + createMutation successfully and lifecycle hooks fire. The index route (/) — default ssr: true — exhibits the bug. This is consistent across browser sessions and across pnpm dev restarts.
createEffectandonMountdon't fire on hydrated routes (Solid Start + TanStack Solid Router)Summary
For routes rendered via Solid Start (SSR + client hydration), Solid lifecycle hooks (
createEffect,onMount) never fire on the client, even though:onCleanupdoes fire (so a Solid scope is being created and then disposed)setTimeout,queueMicrotask,requestAnimationFrame,Promise.then) all fire normallyRoutes with
ssr: falseare unaffected — they go through a fresh-render path on the client and lifecycle hooks work as expected.Versions
@tanstack/solid-start:^1.168.13@tanstack/solid-router:^1.170.8@tanstack/solid-router-ssr-query:^1.167.0@tanstack/router-plugin:^1.168.11solid-js:^1.9.13vite:^8.0.0Minimal reproduction
Any default-SSR route:
Observed client console output
Missing:
3. createEffectand4. onMount.Expected
createEffectandonMountshould fire on the client during/after hydration, as they do for non-routed Solid components and forssr: falseroutes.Interpretation
onCleanupfiring strongly suggests the route component is called inside a disposable scope (probably as part of route resolution / dry-run for matching) that is torn down before reactive effects can flush. The actual interactive DOM is then hydrated from SSR'd HTML in a separate scope where the function isn't re-invoked, so lifecycle hooks scheduled by the first invocation never run.If that's the intended design, it should probably be documented prominently (since
onMount-for-client-side-init is a near-universal Solid pattern). If it's not intended, the route component's reactive scope needs to be allowed to flush effects before disposal.Workarounds (currently in use)
For SSR'd routes:
!isServerguard — works for idempotent operations (writing todocumentElement, preloading images, etc.).ssr: falseon the route — gives up SSR for the route but lifecycle hooks then work normally.setTimeout(() => ..., 0)from the render body — defers past the disposed scope, fires reliably.Cross-route observation
In our app,
/account/children(which hasssr: false) usescreateQuery+createMutationsuccessfully and lifecycle hooks fire. The index route (/) — defaultssr: true— exhibits the bug. This is consistent across browser sessions and acrosspnpm devrestarts.