Mimir patterns
Concrete, copy-pasteable patterns for the state you actually build. Each is small on purpose, Mimir's value
is that these stay small.
Theme toggle (persisted, no flicker)
0000000000// atoms/theme.ts
import { atom } from "ekko:rune/mimir";
export const themeAtom = atom({ key: "site-theme", default: "dark" });
0000000000// a toggle anywhere
import { useAtom } from "ekko:rune/mimir";
import { themeAtom } from "../atoms/theme";
export function ThemeToggle() {
const [theme, setTheme] = useAtom(themeAtom); return <button onClick={() => setTheme(t => t === "dark" ? "light" : "dark")}>{theme}</button>; }
Seed it from ssr() and apply the .dark class with the no-FOUC script so the first paint is correct
too (see No-FOUC and Theming). With a domain session it
survives reloads.
A bridge that mirrors an atom to the DOM
Theming needs the .dark class on <html>; subscribe to the atom and toggle the class:
0000000000import { useEffect } from "@ekko/react";
import { mimir } from "ekko:rune/mimir";
import { themeAtom } from "../atoms/theme";
export default function ThemeBridge() {
const apply = (v: string) => document.documentElement.classList.toggle("dark", v !== "light"); apply(mimir.get(themeAtom)); return mimir.subscribe(themeAtom, apply); }
Keep an in-progress form in an atom so leaving and returning (or an accidental nav) does not lose it:
0000000000const draftAtom = atom({ key: "contact:draft", default: { name: "", message: "" } });
function ContactForm() {
const [draft, setDraft] = useAtom(draftAtom); <input value={draft.name} onChange={e => setDraft(d => ({ ...d, name: e.target.value }))} /> <textarea value={draft.message} onChange={e => setDraft(d => ({ ...d, message: e.target.value }))} /> }
Mark it persist: false if a reload should clear it; leave the default persist: true (with a session) to
keep it across reloads.
A persisted list (todo / cart): add, toggle, remove
The most common real case is an array of objects you add to, toggle, and remove, persisted across a
reload. Keep the list in one atom and update it immutably with the functional set(prev => …) form:
0000000000import { atom, useAtom } from "ekko:rune/mimir";
type Todo = { id: string; text: string; done: boolean };
export const todosAtom = atom<Todo[]>({ key: "todos:list", default: [], persist: true });
function Todos() {
const [todos, setTodos] = useAtom(todosAtom); const add = (text: string) => setTodos(list => [...list, { id: crypto.randomUUID(), text, done: false }]); const toggle = (id: string) => setTodos(list => list.map(t => t.id === id ? { ...t, done: !t.done } : t)); const remove = (id: string) => setTodos(list => list.filter(t => t.id !== id)); <input type="checkbox" checked={t.done} onChange={() => toggle(t.id)} /> <button onClick={() => remove(t.id)}>delete</button> }
Because todosAtom is persist: true and the session is active, the
list survives a full reload. Always update with the functional form (set(prev => …)) so concurrent updates
don't clobber each other. (Verify persistence in a real browser, not via curl, the server renders the
default [].)
Async data (load once, share everywhere)
Store fetched data in an atom plus a loading flag; many components read the same result:
0000000000const usersAtom = atom({ key: "users:list", default: [] as User[] });
const loadingAtom = atom({ key: "users:loading", default: false });
async function loadUsers() {
mimir.set(loadingAtom, true); const res = await fetch("/api/users"); mimir.set(usersAtom, await res.json()); mimir.set(loadingAtom, false); }
0000000000function UserList() {
const users = useAtomValue(usersAtom); const loading = useAtomValue(loadingAtom); if (loading) return <Spinner />; return <ul>{users.map(u => <li key={u.id}>{u.name}</li>)}</ul>; }
Trigger loadUsers() from an effect or an event; every component reading usersAtom updates together. See
Data fetching.
Derived totals with a selector
0000000000const cartAtom = atom({ key: "cart:items", default: [] as Item[] });
export const cartCount = selector({ key: "cart:count", get: ({ get }) => get(cartAtom).length });
export const cartTotal = selector({
get: ({ get }) => get(cartAtom).reduce((s, i) => s + i.price * i.qty, 0), });
The header badge reads cartCount, the checkout reads cartTotal, both update precisely when the cart
changes, never on unrelated state. See Selectors.
A filtered, sorted view
0000000000const queryAtom = atom({ key: "search:q", default: "" });
const itemsAtom = atom({ key: "items", default: [] as Item[] });
export const visibleItems = selector({
const q = get(queryAtom).toLowerCase(); return get(itemsAtom).filter(i => i.name.toLowerCase().includes(q)).sort(byName); });
The list view reads visibleItems; typing in the search box sets queryAtom; the selector recomputes. No
manual recompute calls, no stale lists.
Cross-component coordination (no prop drilling)
A sidebar's open/closed state, read by the toggle button, the overlay, and the layout, lives in one atom:
0000000000export const sidebarOpen = atom({ key: "ui:sidebar-open", default: false });
The button calls useSetAtom(sidebarOpen); the overlay and layout useAtomValue(sidebarOpen). They are
nowhere near each other in the tree, and there is no context provider, they just import the same atom.
Next: the mistakes to avoid, Pitfalls.