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)
Hydration
Hydration is the moment a static, server-rendered page becomes a live React app without throwing away the HTML the server already sent. rune does this for you; this page explains the machinery so you can reason about it (and debug it).
The payload: __EKKO_DATA__
Every rune document ends with a JSON script the server emitted:
Each field has a job:
| Field | Used for |
|---|---|
page | The current page's client chunk to hydrate. |
__layout | The shared layout chunk wrapped around every page. |
__routes | The client route table the router matches navigations against. |
__atoms | Initial Mimir state (see SSR & hydration). |
__routerConfig | Back-button rules and before-unload guards. |
__user | An optional authenticated user object. |
__sessionMode | Mimir's persistence mode (none / ephemeral / domain). |
props | params, query, path for the current request. |
The four steps
- Parse
__EKKO_DATA__. - Seed Mimir with
__atomsviamimir.initStore(...). Plain values fill any unset atom;{__force}
overwrites; {__merge} deep-merges into an existing object. This is what makes useAtom read the
server's value on the very first client render, so there is no flash of default state.
- Import the layout chunk and the page chunk (dynamic
import()from/_ekko/...). - Hydrate
layout(page)onto the existing DOM. React walks the server markup and attaches event
handlers instead of recreating nodes.
Why there is no hydration mismatch
The classic SSR bug is the server and client rendering different trees, producing console warnings and flicker. rune avoids it structurally:
- The server composes the element tree as
rootLayout(page)(see_renderBodyinekko:rune). - The client composes the identical
layout(page). - The data both trees read comes from the same place: atoms seeded from
__atoms.
Same markup, same data, clean attach.
Practical implications
- Do not read browser-only globals (
window,document,localStorage) during render in a way that
changes the output, the server cannot see them, so the trees would diverge. Read them in an effect (after
hydration) or seed the value through an atom in ssr().
- Theme without flicker: seed the theme atom in
ssr()and set the.darkclass with a tiny inline
script before first paint (the "no-FOUC" pattern). See No-FOUC.
- Per-request data: values that depend on the URL belong in
props(params/query/path) for shell
routes, or are fetched after hydration. Static ssr() results are cached and shared, so do not put
per-user data in a cached static route's __atoms.
After hydration
The router is now live. Navigations are handled entirely on the client by matching __routes and importing
chunks (see The router and Navigation). The server
only sees the next request if the user does a hard reload or hits a dynamic/shell route directly.
Next: where the chunk URLs come from, The manifest.