Hydration utilities
Activate Rho's build-time "static HTML with marker classes" into interactive components in the browser.
What needs hydration
| DSL | Needs hydration |
|---|---|
| interact (slider / input / select / toggle / button / template / computed / timer / vega / svg) | ✅ |
| tabs (click to switch) | ✅ |
| stepper (Prev/Next) | ✅ |
| modal (click to expand) | ✅ |
| ─────────────────────────────── | |
| Callout | ❌ Static HTML |
| Layout grid + cards | ❌ CSS Grid |
| Annotate | ❌ CSS hover |
| Timeline | ❌ CSS layout |
| STL syntax highlight | ❌ highlight.js auto |
hydrateAll(container) — one-shot activate
The most-used API. Scans all Rho marker classes in the container and takes them over in turn.
import { hydrateAll } from '@rho/md';
const container = document.getElementById('root');
container.innerHTML = await renderRho(markdown);
hydrateAll(container);
Idempotent — safe to call multiple times on the same container. Each hydrated block is marked with data-rho-hydrated="true"; subsequent calls skip.
Equivalent to:
renderInteractBlocks(container);
renderTabsBlocks(container);
renderStepperBlocks(container);
renderModalBlocks(container);
Individual hydrate
For fine-grained control (lazy load / performance tuning / partial takeover):
import {
renderInteractBlocks,
renderTabsBlocks,
renderStepperBlocks,
renderModalBlocks,
} from '@rho/md';
// Hydrate tabs only (structural); defer interact (heavy)
renderTabsBlocks(container);
// Later, on tab switch, hydrate the active panel's interact
container.querySelector('.rho-tab-btn').addEventListener('click', () => {
const activePanel = container.querySelector('.rho-tab-panel:not([hidden])');
renderInteractBlocks(activePanel);
});
Hydration timing
Pattern 1: hydrate immediately after render (default)
container.innerHTML = html;
hydrateAll(container);
Best for: small content where everything's visible right away.
Pattern 2: hydrate when scrolled into view (lazy)
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
hydrateAll(entry.target);
observer.unobserve(entry.target);
}
});
});
container.querySelectorAll('.rho-interact, .rho-tabs, .rho-stepper').forEach((el) => {
observer.observe(el);
});
Best for: long documents with many charts — avoid first-paint lag.
Pattern 3: hydrate only on user trigger
container.innerHTML = html;
const button = document.getElementById('activate-demos');
button.addEventListener('click', () => {
hydrateAll(container);
button.remove();
});
Best for: extreme performance optimization scenarios; explicitly tells users "click to enable demos."
Hydration options
Some hydrate functions accept options (note: option types may evolve across versions):
import { hydrateAll } from '@rho/md';
import type { HydrationOptions } from '@rho/md';
const options: HydrationOptions = {
// Callback after each block hydrates
onBlockHydrated: (blockEl: HTMLElement, type: string) => {
console.log(`Hydrated ${type}`);
},
// Fallback UI when hydration fails
errorBoundary: (blockEl: HTMLElement, error: Error) => {
blockEl.innerHTML = `<p style="color: red">Failed to hydrate: ${error.message}</p>`;
},
// Skip specific marker classes (default: hydrate all)
skip: ['rho-modal'],
};
hydrateAll(container, options);
Re-hydrate (content changed)
When markdown source changes → re-process → re-inject into container:
async function update(newMarkdown: string) {
container.innerHTML = await renderRho(newMarkdown);
hydrateAll(container);
}
Why hydrateAll is still safe: the previous hydrated DOM was replaced by innerHTML = ...; the new DOM has no data-rho-hydrated and gets hydrated normally.
⚠️ When you replace the DOM, previous interact state (slider position / current values in namespace computeds) is lost — hydrate is "static HTML → live," not "live → live." To preserve state, use a framework component approach (manage state in React/Vue).
Hydration order with nesting
hydrateAll defaults to outer-first. Example:
<div class="rho-tabs"> ← 1. Hydrate tabs first
<div class="rho-tab-panel">
<div class="rho-interact"> ← 2. Then hydrate interact
<div class="rho-modal-trigger"> ← 3. Modal last
</div>
</div>
</div>
Usually you don't care — hydrate is declarative; the result is consistent. But knowing this helps when debugging hydrate-order issues.
Server-side rendering (SSR) + hydration
// === SSR (Node.js) ===
const html = String(await processor.process(markdown));
const fullPage = `
<!DOCTYPE html>
<html>
<head>
<link rel="stylesheet" href="/@rho/md/css/stl-md.css">
</head>
<body>
<div id="root" class="markdown-body">${html}</div>
<script type="module" src="/client.js"></script>
</body>
</html>
`;
return fullPage;
// === client.js (browser) ===
import { hydrateAll } from '@rho/md';
hydrateAll(document.getElementById('root')!);
Key: SSR only emits HTML (with marker classes); hydrate must run in the browser.
React / Vue / Svelte integration
React (useEffect)
import { useEffect, useRef } from 'react';
import { hydrateAll } from '@rho/md';
function RhoMarkdown({ source }: { source: string }) {
const ref = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!ref.current) return;
(async () => {
ref.current!.innerHTML = await renderRho(source);
hydrateAll(ref.current!);
})();
}, [source]);
return <div ref={ref} className="markdown-body" />;
}
Vue 3
<script setup lang="ts">
import { ref, onMounted, watch } from 'vue';
import { hydrateAll } from '@rho/md';
const props = defineProps<{ source: string }>();
const root = ref<HTMLElement>();
const update = async () => {
if (!root.value) return;
root.value.innerHTML = await renderRho(props.source);
hydrateAll(root.value);
};
onMounted(update);
watch(() => props.source, update);
</script>
<template>
<div ref="root" class="markdown-body" />
</template>
Svelte
<script lang="ts">
import { onMount } from 'svelte';
import { hydrateAll } from '@rho/md';
export let source: string;
let root: HTMLDivElement;
onMount(async () => {
root.innerHTML = await renderRho(source);
hydrateAll(root);
});
</script>
<div bind:this={root} class="markdown-body" />
Performance tips
- One page hydrating 20+ interact blocks → consider lazy hydrate
- First paint, only hydrate visible blocks → hydrate the rest on scroll
- Big charts (vega-lite) lag first paint → let users click to trigger hydration
- Repeated namespace-shared interact blocks → reactive updates compute once
See also
- Quick start — Standard integration pattern
- API reference
- Remark plugins — Build-time output marker classes
- Inner processor — Nested hydration