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`.
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 `<Frame>` 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()
awaitfetch(routes.cart.add.href(),{
method:'POST',
body: newFormData(event.currentTarget),
signal,
})
if(signal.aborted)return
awaithandle.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:
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:
thrownewError(`Unable to resolve client entry export for ${entryId}`)
}
return{
href: awaitassetServer.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 `<script type="application/json">` tag.
Client entry props must be serializable: strings, numbers, booleans, `null`, `undefined`, plain objects/arrays of the above, JSX elements, and `<Frame>` elements. Functions and class instances cannot be passed.
## Booting the Client
Use `run` to start the client runtime. It scans the document for client entry markers, loads modules, and hydrates each one:
- **`resolveFrame(src, options)`** (optional) — called when a `<Frame>` loads or reloads content and for intercepted link and form navigations. `options` may contain `signal` and `target`; non-GET forms also provide `formData`, `method`, and `encType`.
A `<Frame>` renders server content into the page. Frames stream after the initial HTML, nest inside other frames, contain client entries, and can be reloaded without full page navigation.
When `run({ resolveFrame })` is active, eligible same-origin forms progressively enhance into frame navigations. Native validation and the form's `submit` event still run first.
- Forms target `handle.frames.top` by default.
-`rmx-target` selects a named frame.
-`rmx-src` selects a different frame request URL while preserving the form action as the navigation destination.
-`rmx-history="push|replace"` overrides how the navigation updates history.
-`rmx-document` opts back into a document submission.
- Cross-origin forms, `method="dialog"`, and `target="_blank"` remain browser-owned.
GET controls are already encoded in `src`, so GET forms reach the resolver like links. Non-GET forms provide their native `FormData`, effective method, and encoding. The resolver owns body encoding and method-override conventions. Non-GET submissions to the current URL replace its history entry; GET submissions and submissions to a different URL push one. The `rmx-history` attribute overrides those defaults.
Frames can nest. Each frame owns its own DOM region and hydrates client entries independently. During SSR, `handle.frame.src` points at the frame being rendered, while `handle.frames.top.src` stays fixed at the outer document URL.
## Server Rendering
### `renderToStream`
Renders a component tree to a `ReadableStream<Uint8Array>`. Sends initial HTML immediately and streams frame content as it resolves:
- **`frameSrc`** — seeds SSR frame state; populates `handle.frame.src` and `handle.frames.top.src`
- **`topFrameSrc`** — overrides the root frame URL for nested frame renders (carry forward from `resolveFrame` context)
- **`resolveFrame(src, target, context)`** — return HTML string, `ReadableStream<Uint8Array>`, or a promise of either. `context.currentFrameSrc` is the containing frame URL; `context.topFrameSrc` is the outer document URL
- **`onError(error)`** — called on rendering errors
### `renderToString`
Renders a component tree to a complete HTML string. Use for static pages or embedding HTML:
```tsx
import{renderToString}from'remix/ui/server'
lethtml=awaitrenderToString(<App/>)
```
### CSS in SSR
Components using the `css` mixin have styles collected during rendering and emitted as a single `<style>` tag in `<head>`. No client-side style injection needed.
## Navigation
Use real anchors for normal document navigation. For app-driven navigation:
-`navigate(href, options?)` — performs a Navigation API transition
-`link(href, options?)` mixin — makes any element behave like a navigation link
Manage document head with an explicit `<head>` in your document structure:
```tsx
functionApp() {
return()=>(
<html>
<head>
<title>Dashboard</title>
<metaname="description"content="Team dashboard"/>
<linkrel="stylesheet"href="/styles/app.css"/>
</head>
<body>
<main>...</main>
</body>
</html>
)
}
```
Put `title`, `meta`, `link`, and `style` tags inside an explicit `<head>`. Bare head-like tags rendered outside `<head>` stay where they are — they are not moved into the document head for you.