Error: Hydration failed because the initial UI does not match what was rendered on the server – Remix loader after schema change
0 reputation · 12 Sept 2024, 23:02 UTC
0 reputation · 12 Sept 2024, 23:02 UTC
The goal is to understand whether Remix should automatically revert loader data to its pre‑mutation state when an action throws an error, thereby avoiding a hydration mismatch that occurs when the loader returns a shape altered by a schema change.
Constraints include maintaining developer control over error handling, ensuring compatibility across Remix v1 and v2, and not interfering with custom error boundaries or useTransition hooks that already manage rollback logic.
29775 reputation · 13 Sept 2024, 09:26 UTC
No — Remix should not (and does not) automatically roll back loader data when an action throws, and that is not what is causing your hydration error. These are two separate concerns that are easy to conflate when they surface together after a schema change.
The hydration error means the HTML your server rendered does not structurally match what React produced on the client's first render. After a schema change, the classic cause is stale loader data: the server rendered with the new field shape, but the client revalidated against a cached loader JSON response (browser cache, service worker, or CDN) that still has the old shape. A conditional render like {data.newField && <span>…</span>} then takes different branches on each side, and hydration fails.
Action failure is a red herring here. When an action throws, Remix renders your route's ErrorBoundary with the existing loader data still in place — it does not mutate loader data, so there is nothing to "roll back." Loader data only changes on revalidation, which Remix triggers after a successful action. If you are seeing data revert visually, that is your own rollback logic in useTransition/useNavigation or an error boundary rendering different markup — and a boundary that renders different structure than the normal branch is itself a legitimate source of hydration mismatches if it renders during the initial pass.
{data.newField ?? null}, rather than branching on existence.Date.now(), Math.random(), locale formatting, or typeof window checks in render.Automatic rollback of loader data on action failure would be the wrong default: the loader data was never changed by the action, and synthesizing a "previous" state would fight with error boundaries, which exist precisely so the application decides what a failure looks like. Keep rollback in application code — optimistic UI via useNavigation (v2) / useTransition (v1) with explicit revert is the supported pattern.
Data-loading internals differ across Remix versions: v1 uses per-route loader fetches, while v2 (later releases) moved toward a single-fetch strategy, which changes which URLs carry loader data and which cache headers apply. Before tuning cache behavior, confirm your version's data strategy in the Network tab rather than assuming. Also note useTransition was renamed to useNavigation in v2 — the rollback logic itself is unchanged.
One caveat: if your error boundary renders during the initial server pass (e.g., the loader itself throws after the schema change because old cached data fails validation), the mismatch may come from the boundary markup. Check whether the server document contains your error UI — if so, fix the loader's handling of legacy-shaped data rather than the hydration layer.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.