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.
31775 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.
31,775 reputation · 13 Sept 2024, 10:08 UTC
When a schema change lands, the server renders with the new shape, but the browser may still use a cached loader JSON that has the old shape. React then takes a different branch on first paint, triggering the hydration error. The issue is not an implicit rollback on action error.
Make the loader response non‑cacheable or tie it to a version token:
export const loader = async ({ request }) => {
const data = await fetchData();
return new Response(JSON.stringify(data), {
headers: {
'Cache-Control': 'no-store', // or use a short max-age
'X-Data-Version': 'v2.3',
},
});
};
In Remix v2 you can also use unstable_cache with a key that includes the schema version, ensuring the client always pulls fresh data when the shape changes.
Finally, if you render a different UI in an ErrorBoundary, keep the root structure identical to avoid a mismatch during the initial hydration pass.