Attaches event listeners and makes existing server-rendered markup interactive.
Signature
function hydrate(
vnode: ComponentChild,
parent: ContainerNode
): void
Parameters
The virtual node tree that matches the existing server-rendered HTML structure.
The DOM element containing the server-rendered content to hydrate.
Return Value
Returns void. The function performs a side effect by hydrating the existing DOM.
Description
The hydrate function is used to “hydrate” existing server-rendered HTML markup. Instead of creating new DOM nodes, it attaches event listeners and internal state to the existing DOM structure, making it interactive.
This is particularly useful for:
- Server-side rendering (SSR) scenarios
- Improving initial page load performance
- Progressive enhancement strategies
Implementation
The hydrate function is implemented in src/render.js:66 and works by:
- Setting the
MODE_HYDRATE flag on the vnode
- Calling the
render function with the hydration flag enabled
- Reusing existing DOM nodes instead of creating new ones
export function hydrate(vnode, parentDom) {
vnode._flags |= MODE_HYDRATE;
render(vnode, parentDom);
}
Usage Examples
Basic Hydration
import { hydrate, h } from 'preact';
function App() {
return (
<div>
<h1>Hello World</h1>
<button onClick={() => alert('Clicked!')}>Click me</button>
</div>
);
}
// Server-rendered HTML exists in the DOM
// Hydrate attaches event listeners without recreating elements
hydrate(<App />, document.getElementById('root'));
Hydration with Data
import { hydrate, h } from 'preact';
import { useState } from 'preact/hooks';
function Counter({ initialCount }) {
const [count, setCount] = useState(initialCount);
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
</div>
);
}
// Assume server rendered with initialCount=0
hydrate(<Counter initialCount={0} />, document.getElementById('root'));
Full SSR + Hydration Flow
// server.js
import { render as renderToString } from 'preact-render-to-string';
import { App } from './App';
const html = renderToString(<App />);
res.send(`
<!DOCTYPE html>
<html>
<body>
<div id="root">${html}</div>
<script src="/bundle.js"></script>
</body>
</html>
`);
// client.js
import { hydrate } from 'preact';
import { App } from './App';
hydrate(<App />, document.getElementById('root'));
Important Considerations
Markup Mismatch
The virtual node tree passed to hydrate must match the existing HTML structure. Mismatches can cause:
- Unexpected behavior
- Loss of hydration benefits
- Console warnings in development
// Server rendered:
<div><p>Hello</p></div>
// ❌ Wrong - structure mismatch
hydrate(<div><span>Hello</span></div>, root);
// ✅ Correct - matching structure
hydrate(<div><p>Hello</p></div>, root);
Event Handlers
Event handlers are not included in server-rendered HTML. The hydrate function attaches them during hydration:
// Server renders: <button>Click me</button>
// Hydrate adds: onClick handler
hydrate(
<button onClick={() => console.log('Clicked!')}>Click me</button>,
root
);
Differences from render
render: Creates new DOM nodes, replacing existing content
hydrate: Reuses existing DOM nodes, only attaching handlers and state
render - Standard rendering without hydration
h - Create virtual nodes
preact-render-to-string - Server-side rendering utility