Skip to main content
Attaches event listeners and makes existing server-rendered markup interactive.

Signature

function hydrate(
  vnode: ComponentChild,
  parent: ContainerNode
): void

Parameters

vnode
ComponentChild
required
The virtual node tree that matches the existing server-rendered HTML structure.
parent
ContainerNode
required
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:
  1. Setting the MODE_HYDRATE flag on the vnode
  2. Calling the render function with the hydration flag enabled
  3. 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

Build docs developers (and LLMs) love