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)
Tutorial , 5. SSR and data
Our list page already server-renders (it is a static route with ssr()). In this step we make the render
correct on the first paint: seed the theme so there is no flash, and fix the no-FOUC class. We also make
the note list a fully cached SSR page.
The theme flash, and why
When the page first paints, the server rendered with the theme atom's default. If the user previously chose "light", the client will switch to light after hydration, a brief flash of dark. Two fixes work together:
- Seed the atom in
ssr()so the React tree is right. - Set the
.darkclass before paint with a tiny inline script reading the same persisted preference.
Seed the theme atom
For a per-user choice you would read it from a cookie/header here. But our list is a cached static page (shared across users), so we only seed a neutral default and let the client's persisted session take over after hydration, exactly the caveat from Mimir → SSR & hydration.
The no-FOUC script
Add an inline script to the document head that sets .dark from the persisted theme before the browser
paints. The client persisted the theme under the Mimir IndexedDB store, but for the very first paint we want
a synchronous read, the simplest robust approach is to also mirror the choice to localStorage in the
bridge and read it here:
Now the first paint matches the user's choice (the class is set before render), the seeded atom matches the React tree, and the persisted session keeps it across reloads. No flash. This is the standard rune theming recipe, see No-FOUC.
The note list is cached SSR
Because / is static and exports ssr(), rune renders it into the cache, eagerly at startup with our
ssr: "eager" setting. The first visitor gets a warm, fully-rendered page (good for first paint and SEO).
When we add notes via the API in the next step, that cache would go stale. We will call app.invalidate("/")
(or a tag) after a write so the list re-renders. For now, since notes live only in the client atom, the
list page renders the seed notes on the server and the client atom takes over after hydration.
Per-request data on the detail page
The detail route /notes/:id is dynamic, so it renders on the client from params. Its ssr() can
still set a useful title. If you wanted the title to include the note's name on first load (for shared
links), you would either:
- look the note up in
ssr()if your data is available server-side (it is,notesAtom's default is
seedNotes), or
- register one static route per known note id (so each gets a cached render and a precise title), the
programmatic routes technique.
For a notes app whose ids are user-created at runtime, the dynamic route + client render is the right call; for a fixed content set (docs, products), prefer per-entry static routes.
Verify
- View source on
/, the list HTML is there, server-rendered. - Set the theme to light, reload, no flash to dark; it stays light from the first paint.
- The server log shows
SSR cache: N pages rendered (eager).
Next: persist notes for real with an API, 6. API routes.