Solid SSR Hydration Debug Playbook
Goal
Find the exact mismatch source fast, then restore features with targeted hydration-safe fixes.
Isolation Method
Use coarse-to-fine comment/backout isolation.
- Disable a large suspect subtree.
- Confirm hydration stabilizes.
- Re-enable one chunk at a time.
- Stop when mismatch returns.
- Rewrite only that chunk with hydration-safe patterns.
Practical Sequence (General)
- Disable custom render overrides first (high signal).
- Restore plain baseline render path.
- Re-enable headings only.
- Re-enable
codebehavior only. - Re-enable
prewrappers/controls. - Re-enable mermaid/client-only visuals last.
Practical Sequence (Async Route Pages)
- Reduce route to static content (no
createAsync, no list rendering). - Re-enable one async section only.
- Re-enable second async section only.
- If each works alone but fails together, split into separate
Suspenseislands. - Re-enable list iteration in steps:
- scalar render (
countonly) - list loop with native elements
- styled/components wrappers
- scalar render (
- Keep each suspect step as a single small delta so regressions are attributable.
Fix Patterns
- Keep SSR and initial client DOM identical.
- Move dynamic branching to
onMountonly when truly client-only behavior is needed. - Prefer one async resource per
Suspenseboundary for unrelated sections. - Replace shared mixed-resource list gates with section-local gates.
- Keep list ordering deterministic across SSR/client.
- If
grouped().lengthworks butFor each={grouped()}fails, suspect row subtree hydration ownership and isolate row markup/component boundaries.
Debug Signals
template2 is not a functionwith content flash: likely tree mismatch.Hydration Mismatch. Unable to find DOM nodes for hydration key ...withdata-source-loc: use the source location to isolate that exact JSX subtree first.- Routes with Ark/overlay selects that stabilize when
skipPortalis enabled: treat portal ownership as the first suspect. - Works when one section is removed, fails when both are present: boundary interaction problem.
- Works until one feature re-enabled: culprit localized.
ReferenceError: document is not definedwith@zag-js/splitterframes (removeGlobalCursor/clearGlobalCursor): Splitter machine cleanup is running in server context.
Recent Incident Pattern (Vacation Planner)
Use this order for similar failures:
- Check for framework machine stack traces first.
- If stack includes
@zag-js/splitter, remove/gate Splitter immediately and verify with static CSS columns.
- If stack includes
- Revert pre-hydration "effective state" memos that can change
Showgates.- Example: derived IDs that pick first record (
selected || firstItem) caused route subtree ownership flips during hydration.
- Example: derived IDs that pick first record (
- Disable portalling in select/popover wrappers on SSR routes.
- Keep overlays in-tree first; only re-enable portals after hydration is stable.
- Re-run verification in this order:
- hard refresh
/ - direct deep-link URL load
- in-app navigation between tabs/views
pnpm type-check
- hard refresh
Example Prompts
- "Use the hydration debug playbook: comment out risky chunks, isolate the mismatch, then re-enable features incrementally."
- "I get a Solid hydration error on refresh. Apply the backout-and-restore workflow and fix only the culprit area."
- "Run a coarse-to-fine SSR mismatch triage on this component and propose targeted post-mount fixes."
Completion Criteria
- Hard refresh loads without hydration error.
- Server render and client hydration both succeed.
- Required features restored incrementally.
- No broad rewrites outside culprit area.
Verification
Run pnpm type-check.
Validate with hard refresh, direct URL load, and in-app navigation.