Hydration

Hydration is the process of taking server-rendered HTML and making it interactive by attaching JavaScript event handlers and component state. Crank provides robust hydration capabilities that handle complex scenarios while maintaining performance and developer experience.

Basic Hydration

The DOM renderer provides a hydrate() method that attempts to reuse existing DOM nodes instead of creating new ones:

import {renderer} from "@b9g/crank/dom";

// Server-rendered HTML exists in the DOM
// <div id="app"><button>Count: 0</button></div>

function *Counter() {
let count = 0;
const onclick = () => this.refresh(() => count++);

for ({} of this) {
yield (
<button onclick={onclick}>
Count: {count}
</button>
);
}
}

// Hydrate instead of render to reuse existing DOM
renderer.hydrate(<Counter />, document.getElementById("app"));

During hydration, Crank will:

Unlike React, hydration will always choose the client render. The server render will be blown away if there mismatches.

Hydration Warnings

In development, Crank provides comprehensive warnings when server and client rendering don't match:

// Because this component renders differently on client and server you will
// receive a warning
function TimeDisplay() {
return <div>Client time: {new Date().toDateString()}</div>;
}

The hydrate prop

You can control hydration and squash warnings with the special hydrate prop:

Skipping Hydration

Use hydrate={false} to skip hydration for elements that should remain static:

function App({userContent}) {
return (
<div>
<nav>
<button onclick={() => console.log("Interactive!")}>
Menu
</button>
</nav>

{/* Skip hydration for user-generated content */}
<div hydrate={false} innerHTML={userContent} />

{/* Skip hydration for third-party widgets */}
<div hydrate={false} id="third-party-widget" />
</div>
);
}

This is particularly useful for:

Forcing Hydration

Use hydrate={true} to force hydration for nested portals:

import {Portal} from "@b9g/crank";

function Modal({children}) {
return (
<Portal root={document.body} hydrate={true}>
<div class="modal">
{children}
</div>
</Portal>
);
}

Portal children are normally skipped during hydration, but hydrate={true} ensures their children are included.

Selective Prop Hydration

Use string values to specify which props should not emit hydration errors:

You cannot mix bang (!) and non-bang syntax. Doing so will cause a warning.

function FormInput({value, placeholder}) {
return (
<div>
<input
hydrate="!value"
type="text"
placeholder={placeholder}
value={value}
/>
<div
hydrate="class id"
class="form-group"
id="user-input"
data-analytics="skip-me"
/>
</div>
);
}
let colorScheme =
typeof window !== "undefined"
? sessionStorage.getItem("color-scheme") ??
(window.matchMedia("(prefers-color-scheme: dark)").matches
? "dark"
: "light")
: "light";

export function *ColorSchemeToggle() {
const toggleScheme = () => this.refresh(() => {
colorScheme = colorScheme === "dark" ? "light" : "dark";
sessionStorage.setItem("color-scheme", colorScheme);
});

if (typeof window !== "undefined") {
const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)");
const systemListener = (event: MediaQueryListEvent) =>
this.refresh(() => colorScheme = event.matches ? "dark" : "light");
mediaQuery.addEventListener("change", systemListener);
this.cleanup(() => {
mediaQuery.removeEventListener("change", systemListener);
});
}

for ({} of this) {
if (typeof document !== "undefined") {
const action = colorScheme === "dark" ? "remove" : "add";
document.body.classList[action]("color-scheme-light");
}

// we do not want to hydrate the children or aria-checked because the
// server did not have access to the DOM and it might be mismatched
yield (
<button
onclick={toggleScheme}
role="switch"
aria-label="Set dark mode"
aria-checked={colorScheme === "dark"}
hydrate="!aria-checked !children"
>
Switch to {colorScheme === "dark" ? "light" : "dark"} mode
</button>
);
}
}