Documentation
Docs
Introduction
Getting Started
Tutorial: build an app
Core Concepts
Routing
Server-Side Rendering
Mimir, state management
Pages & Layouts
API Routes
Styling & Theming
Building & Deploying
API Reference
Guides
Recipes
FAQ (use cases)
SSR and hydration
Concepts → Hydration covers the mechanics in full. This page is the SSR-author's view: the practical concerns of making the server render and the client takeover agree.
The contract
The server renders layout(page) with Mimir seeded from ssr().__atoms, and embeds those same atoms (plus
the route table and chunk URLs) in __EKKO_DATA__. The client reads that payload, re-seeds Mimir, imports
the same chunks, and hydrates the same tree. Match the tree and the data, and hydration is a clean attach.
Your job as a page author is to not break that match. Three rules cover it.
Rule 1 , do not branch render output on the environment
If your component renders different HTML on the server than on the client, hydration mismatches. The usual culprit is reading a browser-only value during render:
Fix it by rendering a stable value first and updating in an effect (after hydration), or by seeding the value
through an atom in ssr():
Rule 2 , seed the state the first render needs
If a component reads an atom and that atom should have a server value (theme, locale, a prefetched list),
seed it in ssr().__atoms. Then useAtom returns the right value on the first client render and the markup
matches:
Use __force to override and __merge to deep-merge (see The ssr() function
and Mimir → SSR & hydration).
Rule 3 , do not seed per-user data into a cached, shared page
A static route's HTML is cached and served to everyone. If you seed user-specific data into its
__atoms, the first user's values get baked into the cached HTML and served to the next user. Two safe
options:
- Personalise on the client. Render the cached, neutral page; after hydration, read the user (from an
atom hydrated from __user, or fetch it) and update the UI.
- Use a non-cached route. Dynamic/shell routes render per request, so they can carry per-request data in
props and __user.
rune helps here: cached HTML is served with the current request's __user patched in (the "__user":null
placeholder is replaced per request), so the user object can differ per request even on a cached page,
but the rendered markup is shared. Anything that must change the markup per user cannot live on a
cached static route.
The __user field
If your middleware attaches req.user, rune injects it into the page data as __user (patched into cached
HTML per request). Hydrate it into an auth atom and your components can read the current user without an
extra round-trip:
Verifying hydration
- View source , you should see real page HTML inside
<div id="__ekko">, and a populated
__EKKO_DATA__ script.
- No console warnings , React logs hydration mismatches; a clean console means the trees matched.
- State on first paint , a seeded atom (e.g. theme) should be correct before any effect runs; if you
see a flash to the default, you forgot to seed it (and, for theme, the no-FOUC script, see No-FOUC).
Next: keeping the cache fresh, Caching & invalidation.