Ramonda

Hydration mismatches

A mismatch is the browser rendering something different from what the server sent. Development reports it as RMD007, naming the component and both values. The browser wins — the DOM is corrected — so the page ends up right, but you lose the adoption: those nodes are rebuilt instead of reused, and whatever was attached to them goes too.

The usual causes

A value that is naturally different on each side.

@state now = new Date().toLocaleTimeString(); // ✗ a different time on each side
@state id = Math.random(); // ✗

Compute these in @created({ env: "server" }) and mark them @persist so the client restores rather than recomputes — or use the client-only pattern below.

The blob does not rescue the first spelling, and it is worth knowing why. A field still holding the primitive its own initializer produced is left out of the page: the browser runs the same initializer and arrives at the same value, so writing it down would be bytes for nothing. Date.now() is exactly where "the same initializer" stops being the same value — the browser's call answers later, and there is no server number in the page to overrule it. Computing in @created({ env: "server" }) is what puts a real value in the blob, because a computed value is not the one the initializer produced.

A value the server EMPTIES does travel. this.user = undefined, where the initializer gave something else, is carried as a cleared field rather than an absent one, so the browser does not put the default back. That distinction is deliberate: undefined cannot ride in JSON, and without it a signed-out visitor would see the signed-in name.

Branching on the environment in render().

render() {
  return typeof window === "undefined" ? <Skeleton /> : <Chart />; // ✗
}

This guarantees divergence. (There is deliberately no isServer() helper — calling it in render() is the bug, not the fix.)

Browser-only APIs read during a renderwindow.innerWidth, localStorage, matchMedia — none exist on the server.

The pattern for client-only UI

Render something stable, then fill it in once the browser is running:

export class Chart extends Component {
  @state isClient = false;

  @mounted({ 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 next commit. Two passes is the honest cost of something that genuinely can't be rendered on the server.

Text is not a mismatch

<span>Hello {name}!</span> is three text pieces in your code but one text node in the HTML (HTML can't record the boundaries). Ramonda splits it back apart on hydration — it is handled, and not reported.

Next

  • Testing — including a hydrated page.