TT Lab
Get started
Learn Learning paths Courses

SSR — The Server Draws First

Hydration Does Not Redraw

Continue in TT Lab

In one line

Hydration attaches event handlers to the DOM the server drew; it does not redraw. So when the two sides drew different things, the screen stays quietly wrong.

Why this was needed

The HTML the server built is already on the browser's screen. The reason the client runs the same component once more is not to build the screen but to find out which handler to attach to which DOM node. So when the premise that the two results are the same breaks, the handlers get attached to the wrong nodes.

The React documentation states this point plainly — for values that unavoidably differ (for example, a timestamp) you can use suppressHydrationWarning, but it is an escape hatch that works only one level deep, and React does not patch up mismatched text (hydrateRoot). That means erasing the warning does not make the values match.

The cause is almost always one of four.

Cause Why they diverge
Time The time the server drew and the time the browser drew differ
Random numbers and incrementing ids If you make them separately in two places, they cannot be the same
Locale and time zone The server is on UTC, the browser on the user's time zone
Values only the browser knows localStorage, screen width, cookies

How it works

Gather the place where values are made into one

If you make the time and the random number once on the server and pass them in through the state, both sides use the same value. Planting the initial state in a tag in ssr-core was for exactly this. The same goes for locale and time zone — leave the formatting to Intl.DateTimeFormat, but specify the time zone and the locale explicitly as options and both sides give the same result (Intl.DateTimeFormat). If you write nothing, it follows the defaults of the runtime environment, and those defaults differing between the server and the browser is the whole problem.

An id made with an incrementing counter is especially nasty. The documentation explains why React provides a separate useId — it cannot be guaranteed that the order in which client components are hydrated matches the order in which the server HTML came out, so a global counter cannot line them up (useId).

Defer the values that truly have to differ

There is no way for the server to know a value only the browser knows. In that case the right approach is to make the first render the same on the server and the client, and only then switch to the browser's value. Because the first render is the same, there is no mismatch, and the change happens after hydration has finished.

Mismatches have to be contained

Even the same mismatch has a completely different cost depending on where it came from. A mismatch inside a boundary only requires redrawing that boundary's subtree, but a mismatch outside a boundary makes the whole page get redrawn. You added SSR to show the first screen quickly, and a single mismatch outside a boundary wipes out that gain entirely.

So there are two rules to follow when you build a differ.

What it looks like in the field

The most common report is not "after a refresh a different value shows for a moment and then changes," but "the value is simply wrong." Because hydration does not fix the text, the old value the server drew stays as it is and is updated only at the next state change. So the bug report comes in as "sometimes the numbers don't match," and the steps to reproduce leave out the reload.

The second most common is the case where the warning shows up only in the development environment. Some teams attach suppressHydrationWarning broadly to an upper node to get rid of the warning, but then only the warning disappears and the wrong value stays. On top of that, it works only one level deep, so the mismatches lower down still raise warnings — the more broadly you attach it, the worse only the signal gets.

The last is time zones. If the server draws a date in UTC and the browser draws it in the local time zone, a whole day near midnight is shifted. If QA tests only in the daytime, it is never caught.

The order of the fix is fixed too. First, do not erase the warning; split the causes — is it time, an id, locale, or a value only the browser knows? The first three can be eliminated by gathering the place where values are made into the server alone, and only the last one needs the handling of keeping the first render the same and deferring the change. If you add suppression first without this distinction, it covers even the three you could have fixed.

What you will do in the next lab

You build a differ that takes the server tree and the client tree and finds the mismatched spots along with their paths. It ignores attribute order, stops when the shape is off, allows suppression for only one level, and attaches the nearest boundary to each mismatch. In the end it uses that result to compute what has to be redrawn.