<!--
Sitemap:
- [rxfy](/index): Typed, normalized, reactive state — built on RxJS
- [Comparison](/comparison): rxfy versus Redux Toolkit, MobX, Jotai, TanStack Query, and TanStack DB
- [Inspired by](/inspired-by): the libraries and ideas rxfy grew out of
- [Agent Skills](/agent-skills): Accurate rxfy context for AI coding assistants
- [Examples](/examples): Runnable apps, from client-only to fully synced
- [Changelog](/changelog)
- [Getting Started](/getting-started)
- [Create Store](/getting-started/create-store): Normalized reactive state in a client-only app
- [Add SSR](/getting-started/add-ssr): Render the first paint on the server, hydrate with no refetch
- [Add Sync Client](/getting-started/add-sync-client): The full stack: the server publishes, the client syncs
- [Core Concepts](/core-concepts): The ideas rxfy is built on
- [Observables](/core-concepts/observables): A value that changes over time, that you can subscribe to
- [Normalization](/core-concepts/normalization): Store each entity once, reference it by id
- [Late Unwrapping](/core-concepts/late-unwrapping): Unwrap async state at the leaf, not the trunk
- [Server-Side Rendering](/core-concepts/ssr): Dehydrate on the server, hydrate with no refetch
- [rxfy](/rxfy): The core package: atoms, lenses, models, and states
- [createModel](/rxfy/create-model): Typed entities in normalized storage
- [defineState](/rxfy/define-state): Typed, normalized state shapes with fetching and mutations
- [createAtom](/rxfy/create-atom): A reactive cell with synchronous get, set, and modify
- [createLens](/rxfy/create-lens): A two-way view into part of an Atom
- [React Bindings](/react): Hooks and helpers for using rxfy in React
- [useStateData](/react/use-state-data): Fetch, normalize, and subscribe to a query
- [useStatePagedData](/react/use-state-paged-data): Paginated and infinite-scroll lists
- [useModelStore](/react/use-model-store): Subscribe to one normalized entity by id
- [useAtom](/react/use-atom): Two-way binding for any IAtom
- [Pending](/react/pending): Render pending, rejected, and fulfilled UI for any observable
- [usePending](/react/use-pending): The status value behind Pending
- [useObservable](/react/use-observable): Bind a raw Observable to React
- [Sync Client in React](/react/sync-client): StoreProvider, useSyncClient, and update handles
- [rxfy-client](/framework/client): The framework-agnostic browser sync runtime
- [createSyncClient](/framework/client/create-sync-client): Connect a transport and drive the sync loop
- [readSsrGrants](/framework/client/read-ssr-grants): Lift SSR-embedded channel grants
- [rxfy-server](/framework/server): Bind Drizzle tables, write, and publish sync updates
- [defineResource](/framework/server/define-resource): Tie a Drizzle table to an rxfy model
- [createSync](/framework/server/create-server): Wire a storage adapter, hub, and secret into a Live object
- [createInMemoryHub](/framework/server/hub): The socket-keyed pub/sub backbone
- [Writes](/framework/server/writes): sync.create / sync.update / sync.delete and touch
- [Storage adapters](/framework/server/storage-adapters): Persist writes with Drizzle or in memory
- [Sync messages](/framework/server/messages): What travels between server and client
- [Grants](/framework/server/grants): The server signs what it serves; the client subscribes with the token
- [rxfy-ws](/framework/ws): The default WebSocket transport
- [createWsServer](/framework/ws/server): Attach a Hub to WebSocket connections
- [createWsClient](/framework/ws/client): The browser transport with reconnect and replay
- [Custom transports](/framework/ws/custom-transport): Bring your own ClientTransport
- [Guides](/guides): Task-focused walkthroughs of common rxfy patterns
- [Pagination and infinite scroll](/guides/pagination): Load and append pages into one normalized list
-->

# Add SSR \[Render the first paint on the server, hydrate with no refetch]

This is the second level. You have a working store from
[Create Store](/getting-started/create-store); now render it on the server so the first
paint arrives already fulfilled — no loading flash, no refetch, no hydration mismatch. There
is still **no server yet pushing updates**; that is the next level,
[Add Sync Client](/getting-started/add-sync-client).

Nothing in your components changes. They declare data with [`useStateData`](/react/use-state-data) exactly as before;
all the data is fetched on server and can be cached with any logic you might need, the result is captured in the
registry, serialized into the HTML, and ingested on the client.

:::info\[Requirements]
Models carry a `name` and states a `key` (both required by their types), and `fetchFn` must
run in **both** environments — it fetches on the server during SSR and on the client for
reloads. See [Server-Side Rendering](/core-concepts/ssr) for the full requirements.
:::

## Render on the server

`onAllReady` waits for every Suspense boundary, then you dehydrate the now-complete registry
into a snapshot and return it alongside the HTML. One registry per request — never shared:

```tsx [entry-server.tsx]
import { PassThrough } from "node:stream";
import { StrictMode, Suspense } from "react";
import { renderToPipeableStream } from "react-dom/server";
import { createModelRegistry, dehydrate, hydrationScript } from "rxfy";
import { StoreProvider } from "rxfy-react";
import { App } from "./App";

export function render(url: string): Promise<{ html: string; state: string }> {
  const registry = createModelRegistry(); // one registry per request — never shared

  return new Promise((resolve, reject) => {
    const { pipe } = renderToPipeableStream(
      <StrictMode>
        <StoreProvider registry={registry} ssr>
          <Suspense fallback={null}>
            <App url={url} />
          </Suspense>
        </StoreProvider>
      </StrictMode>,
      {
        // Fires once every fetch has settled and the registry is fully populated.
        onAllReady() {
          const sink = new PassThrough();
          let html = "";
          sink.on("data", (chunk: Buffer) => (html += chunk.toString()));
          sink.on("end", () => {
            resolve({ html, state: hydrationScript(dehydrate(registry)) });
          });
          pipe(sink);
        },
        onError: (error) => reject(error instanceof Error ? error : new Error(String(error))),
      },
    );
  });
}
```

## Inject the snapshot

Your HTTP handler drops the rendered markup and the dehydrated `<script>` into an
`index.html` template with two placeholders:

```ts [server.ts]
import { readFileSync } from "node:fs";
import { render } from "./entry-server";

// index.html carries two placeholders: <!--app-html--> and <!--app-state-->
const template = readFileSync("./index.html", "utf-8");

// Express / Hono / plain node:http — whatever serves your HTML.
app.get("*", async (req, res) => {
  const { html, state } = await render(req.url);
  const page = template
    .replace("<!--app-html-->", html) // the rendered markup
    .replace("<!--app-state-->", state); // the hydrationScript <script> tag
  res.status(200).setHeader("Content-Type", "text/html").end(page);
});
```

## Hydrate on the client

The injected `<script>` populated `window.__RXFY_SSR__`; `StoreProvider ssr` ingests it, so
the first paint is already fulfilled:

```tsx [client.tsx]
import { hydrateRoot } from "react-dom/client";
import { StoreProvider } from "rxfy-react";
import { App } from "./App";

hydrateRoot(
  document.getElementById("root")!,
  <StoreProvider ssr>
    <App url={window.location.pathname} />
  </StoreProvider>,
);
```

Reload the page with the network throttled: the content is in the initial HTML, and the
client hydrates it without a second fetch.

## Next: Add Sync Client

Your store now survives a page load. The last level is
[Add Sync Client](/getting-started/add-sync-client) — a server that writes and publishes, and
a client that subscribes and applies real-time updates.
