Hydration mismatches
A mismatch is the client rendering something different from what the server sent. Development
reports it as RMD007, naming the component and both values.
The client wins — the DOM is patched — so the page is correct afterwards. What you lose is the adoption: nodes get replaced instead of reused, and whatever was attached to them goes with them.
The usual causes
A value that differs by nature.
@state now = new Date().toLocaleTimeString(); // ✗ different on each side
@state id = Math.random(); // ✗
Both produce one value on the server and another on the client. Compute them in
@create({ env: "server" }) and mark them @persist so the client restores rather
than recomputes.
Branching on the environment in render().
render() {
return typeof window === "undefined" ? <Skeleton /> : <Chart />; // ✗
}
This guarantees divergence. The framework deliberately offers no isServer() helper, because
calling it in render() is the bug rather than the fix.
Browser-only APIs read during a render. window.innerWidth, localStorage,
matchMedia — none exist on the server.
The prescribed pattern for client-only UI
Render something stable, then fill it in once the client is running:
export class Chart extends Component {
@state isClient = false;
@mount({ env: "client" })
ready() {
this.isClient = true;
}
render() {
return this.isClient ? <RealChart /> : <Skeleton />;
}
}
The hydrating render still sees false, so it matches the server exactly. The switch happens on
the commit after.
Two passes is the cost, and it is the honest one: something genuinely cannot be rendered on the server, so it is rendered afterwards.
—updates every second
That clock uses the same pattern — its first value is read in @mount({ env: "client" }), not in
a field initializer, because a time rendered on the server would not match the time here.
Text is not a mismatch
<span>Hello {name}!</span> is three text children on the client and one node in the HTML,
because HTML cannot record the boundaries. That is handled — hydration splits the run apart — and
it is not reported.
Assert state, not just markup
When you are checking hydration behaviour in a test, read the state, not only the DOM.
That is a lesson from a real one: removing the router's URL re-read left every test green, because the first render was correct anyway — the route context had captured its options before the state blob was restored. The markup was right while the state was wrong, and only the state showed it.
Next
- Testing — including testing a hydrated page.