packages/component/docs/hydration.md
Hydration makes server-rendered HTML interactive on the client. You mark specific components as client entries, and the client runtime finds them in the page, loads their code, and hydrates them in place.
Only the components you mark are hydrated. The rest of the page stays as static HTML.
Use clientEntry to mark a component for hydration. The first argument is the module URL and export name the client will use to load the component:
import { clientEntry, on, type Handle } from 'remix/component'
export let Counter = clientEntry(
'/assets/counter.js#Counter',
function Counter(handle: Handle, setup: number) {
let count = setup
return (props: { label: string }) => (
<div>
<span>
{props.label}: {count}
</span>
<button
mix={[
on('click', () => {
count++
handle.update()
}),
]}
>
+
</button>
</div>
)
},
)
The format is moduleUrl#ExportName. If you omit the export name, the function's name is used as a fallback.
On the server, clientEntry components render like any other component. The server wraps their output in comment markers and serializes their props into a <script type="application/json"> tag so the client knows what to hydrate and with what data.
Use run to start the client. It scans the document for client entry markers, loads the corresponding modules, and hydrates each one:
import { run } from 'remix/component'
let app = run({
async loadModule(moduleUrl, exportName) {
let mod = await import(moduleUrl)
return mod[exportName]
},
async resolveFrame(src, signal) {
let res = await fetch(src, { headers: { accept: 'text/html' }, signal })
return res.body ?? (await res.text())
},
})
await app.ready()
run optionsloadModule(moduleUrl, exportName) (required) - Called for each client entry found in the page. Return the component function. Typically uses dynamic import().resolveFrame(src, signal, target) (optional) - Called when a <Frame> needs to load or reload content. The examples here only use src and signal, but target is also available when frame targeting matters. If omitted, Remix Component uses a placeholder HTML response (<p>resolve frame unimplemented</p>). See Frames for details.app methodsapp.ready() - Returns a promise that resolves when all initial client entries have been hydrated.app.flush() - Synchronously flushes all pending updates.app.dispose() - Tears down all hydrated components and cleans up.app is also an EventTarget. You can listen for errors from any hydrated component:
app.addEventListener('error', (event) => {
console.error('Component error:', event.error)
})
Client entry props are serialized to JSON. Supported prop types:
null, undefined<Frame> elements in props (serialized as frame descriptors)Functions, class instances, and other non-serializable values cannot be passed as props to client entries.
<!-- rmx:h:id --> / <!-- /rmx:h --> comment markers.<script type="application/json" id="rmx-data"> tag.run parses the data script, discovers the markers, and calls loadModule for each entry.This means: