Solid SSR Change Safety
Goal
Keep the server-rendered DOM tree identical to the initial client hydration tree.
Core Rule
Match node existence and hierarchy between SSR and first client render. Only mutate attributes/text/content after mount.
Change Guardrails
- Keep the same JSX element structure for SSR and initial client render.
- Do not pass already-created JSX elements through ordinary props and then conditionally insert them inside SSR-rendered layout shells. In Solid hydration this can surface as
template2 is not a function. Prefer data props, component references, render callbacks invoked in one stable branch, or explicit call-site slots with stable wrapper nodes. - Avoid render-time branches based on
window,document, viewport size, time, random values, or client-only APIs. - Treat select/menu/popover portal behavior as hydration-sensitive.
- Prefer in-tree rendering (
skipPortal/non-portalled mode) on SSR route surfaces by default. - Only enable portal rendering when required by clipping/stacking constraints, then hard-refresh verify parity.
- Prefer in-tree rendering (
- Avoid returning different element types for the same markdown node (
codemust staycode,premust staypre). - Defer enhancements to
onMountand use client-side DOM mutation for progressive enhancement. - Prefer setting
id,class,data-*,innerHTML, and event hooks post-mount over changing JSX tree shape. - Preserve stable wrapper nodes when adding buttons/overlays; hide/show after hydration with attributes/CSS.
Async Data Guardrails (High Signal)
- Prefer separate
Suspenseislands for independent async resources.- Example: if one section uses
createAsync(fetchA)and another usescreateAsync(fetchB), do not mix both sections under one sharedShow/list subtree unless they are intentionally coupled.
- Example: if one section uses
- Keep each async section’s fallback/empty states local to that section.
- Avoid one section’s unresolved state changing the DOM shape of another section.
- Avoid mixing multiple async accessors inside one list-render gate.
- Gate one list with one resolved accessor where possible.
- Keep list ordering deterministic across server and client.
- Prefer stable lexical comparisons (
a < b ? -1 : 1) over locale-sensitive comparators when hydration stability matters.
- Prefer stable lexical comparisons (
Safe Workflow
- Identify where SSR output is generated.
- Keep a minimal baseline renderer that is hydration-safe.
- Add enhancement logic only in
onMount. - Ensure fallback markup is identical server/client pre-hydration.
- Hard refresh and verify no hydration mismatch.
- For async-heavy pages, validate each async section in isolation before recombining.
Red Flags
- Hydration errors like
template2 is not a functionor missing hydration keys. template2 is not a functionafter adding a panel/header/icon prop, especiallyicon={<Icon />}or other JSX-valued props that are conditionally rendered by a shared shell.- Brief flash of content before crash.
- Failures that disappear when select/content portal is disabled (
skipPortal) indicate portal hydration ownership mismatch. - Component works only when custom renderer is disabled.
- Hash/query navigation state conflicts that change rendered tree at load.
- Page works when one async section is removed, but fails when two async sections render in the same boundary.
Recent Incident Notes (Vacation Planner)
template2failures were triggered by pre-hydration tree-shape changes from derived "effective" selectors that changed majorShowbranches before hydration completed.- Rule: avoid introducing fallback memos that can switch whole route subtrees during initial hydration.
- Prefer stable signal defaults (or route-level serialized state) that do not alter branch ownership between SSR and first client pass.
SimpleSelectportal rendering increased hydration fragility in this app.- Rule: default overlay/select wrappers to non-portalled in SSR surfaces unless a portal is required.
- If portal is required, validate hard-refresh SSR parity explicitly.
- Ark/Zag
SplittersurfacedReferenceError: document is not definedin server/HMR cleanup paths.- Rule: when SSR errors mention
@zag-js/splittercleanup (removeGlobalCursor/clearGlobalCursor), treat Splitter as the first suspect and swap to static CSS split as containment.
- Rule: when SSR errors mention
- Color palette debugger hit
template2 is not a functionwhen a reusablePanelacceptedicon?: JSX.Elementand insertedprops.iconinside a conditional header.- Rule: reusable SSR shells should own their header tree directly or accept serializable/icon-name data, not conditionally reinsert pre-created JSX nodes.
Example Prompts
- "Use the Solid SSR safety skill and refactor this markdown renderer so hydration is stable while keeping features."
- "Before I merge this frontend change, run an SSR safety pass and list any DOM-tree mismatch risks."
- "Apply hydration-safe progressive enhancement rules to this Solid component and avoid render-time client-only branches."
Verification
Run pnpm type-check.
Use hard-refresh/manual navigation checks for hydration behavior.