# Hydration, Frames, and Navigation ## What This Covers How server-rendered UI becomes interactive in the browser, and how the page updates without a full navigation. Read this when the task involves: - Marking a component for client-side hydration with `clientEntry` - Booting the client runtime with `run` - Streaming server content into a region of the page with `` and reloading those regions - Handling browser HMR updates for hydrated entries - Triggering Navigation API transitions with `navigate(...)` or `link(...)` - Server rendering with `renderToStream` or `renderToString` - Managing the document `` For component-local state and updates, see `component-model.md`. For host-element behavior and events, see `mixins-styling-events.md`. For browser asset HMR setup, see `assets-and-browser-modules.md`. ## Server First, Then Hydrate Make the server route correct before adding `clientEntry(...)`. A POST should already do the right thing on its own — return HTML, a redirect, or an error response — and a GET should already render the page the user expects. `clientEntry` exists to layer interactivity on top of UI that already works without it. When server state changes after a mutation, prefer reloading a `` when the UI region already maps cleanly to a server-rendered route. Frames re-fetch the same route, so the rendering logic stays in one place and the client does not need a parallel "state" API. ```tsx on('submit', async (event, signal) => { event.preventDefault() await fetch(routes.cart.add.href(), { method: 'POST', body: new FormData(event.currentTarget), signal, }) if (signal.aborted) return await handle.frames.get('cart-summary')?.reload() }) ``` Use polling or a small JSON state endpoint when the data changes outside this page, or when a tiny shared widget would be heavier to model as a frame. Pick the lightest sync mechanism that preserves clear ownership of rendering logic. ## Client Entries Use `clientEntry` to mark a component for client-side hydration. In source-served apps, prefer the source module's `import.meta.url` as the entry ID and let server rendering map it to the public asset URL: ```tsx import { clientEntry, on, type Handle } from 'remix/ui' export const Counter = clientEntry( import.meta.url, function Counter(handle: Handle<{ initialCount: number; label: string }>) { let count = handle.props.initialCount return () => (
{handle.props.label}: {count}
) }, ) ``` On the server, provide `resolveClientEntry` to `renderToStream(...)` so source file URLs become browser-loadable asset URLs. Keep this resolution in the render helper so component modules do not hard-code deployment-specific asset paths: ```tsx let stream = renderToStream(, { async resolveClientEntry(entryId, component) { let exportName = entryId.split('#')[1] || component.name if (!exportName) { throw new Error(`Unable to resolve client entry export for ${entryId}`) } return { href: await assetServer.getHref(entryId), exportName, } }, }) ``` If the module export name differs from the component function name, include `#ExportName` in the entry ID or return the exact export name from `resolveClientEntry`. A render helper that only supports source-owned entries can also fail fast when `entryId` is not a `file://` URL. On the server, `clientEntry` components render like any other component. The server wraps their output in comment markers and serializes props into a `