Documentation

Docs

Introduction

What is rune

Philosophy

Why rune

Architecture

Getting Started

Installation

Quick start

Project structure

The dev loop

Tutorial: build an app

1. Create the app

2. Pages & routes

3. Layouts

4. State with Mimir

5. SSR & data

6. API routes

7. Styling

8. Build & deploy

Core Concepts

The application object

Rendering pipeline

Hydration

The build manifest

Configuration (ekko.json)

Permissions

Conventions

Routing

File-based routing

Dynamic routes

The router (useRouter)

Navigation & Link

Guards & redirects

Programmatic routes

Server-Side Rendering

Overview

The ssr() function

Strategies (eager/lazy)

SSR → hydration

Caching & invalidation

SEO

Mimir, state management

Overview

Atoms

Reading & writing

Selectors (derived state)

Subscriptions & the store

SSR & hydration

Persistence & sessions

Patterns & recipes

Pitfalls

Pages & Layouts

Pages

Layouts

Error & not-found

API Routes

Defining routes

Request & response

Middleware

helmet

cors

rateLimit

bodyLimit

validateContentType

csrf

requestId

timeout

errorHandler

httpsRedirect

secureCookies

ipFilter

safePath

Validation & options

Styling & Theming

SCSS

Theming (light/dark)

Asgard integration

No flash (no-FOUC)

Building & Deploying

The build

Static assets

Production deploy

API Reference

ekko:rune

ekko:rune/router

ekko:rune/mimir

ekko:rune/seo

ekko:ssr / css

ekko.json schema

CLI commands

Guides

Rune app from scratch

Recipes

Dark mode

Forms

Data fetching

Authentication

Pagination

FAQ (use cases)

Documentation

Docs

Introduction

What is rune

Philosophy

Why rune

Architecture

Getting Started

Installation

Quick start

Project structure

The dev loop

Tutorial: build an app

1. Create the app

2. Pages & routes

3. Layouts

4. State with Mimir

5. SSR & data

6. API routes

7. Styling

8. Build & deploy

Core Concepts

The application object

Rendering pipeline

Hydration

The build manifest

Configuration (ekko.json)

Permissions

Conventions

Routing

File-based routing

Dynamic routes

The router (useRouter)

Navigation & Link

Guards & redirects

Programmatic routes

Server-Side Rendering

Overview

The ssr() function

Strategies (eager/lazy)

SSR → hydration

Caching & invalidation

SEO

Mimir, state management

Overview

Atoms

Reading & writing

Selectors (derived state)

Subscriptions & the store

SSR & hydration

Persistence & sessions

Patterns & recipes

Pitfalls

Pages & Layouts

Pages

Layouts

Error & not-found

API Routes

Defining routes

Request & response

Middleware

helmet

cors

rateLimit

bodyLimit

validateContentType

csrf

requestId

timeout

errorHandler

httpsRedirect

secureCookies

ipFilter

safePath

Validation & options

Styling & Theming

SCSS

Theming (light/dark)

Asgard integration

No flash (no-FOUC)

Building & Deploying

The build

Static assets

Production deploy

API Reference

ekko:rune

ekko:rune/router

ekko:rune/mimir

ekko:rune/seo

ekko:ssr / css

ekko.json schema

CLI commands

Guides

Rune app from scratch

Recipes

Dark mode

Forms

Data fetching

Authentication

Pagination

FAQ (use cases)

Selectors

A selector is read-only state derived from other atoms (or selectors). You declare how to compute a value; Mimir tracks which atoms it read and recomputes (and re-notifies) only when one of those changes.

1
2
3
4
5
6
7
8
9
10
11
12
13
import { atom, selector, useAtomValue } from "ekko:rune/mimir";
 
const cartAtom = atom({ key: "cart:items", default: [] as Item[] });
 
export const cartTotal = selector({
key: "cart:total",
get: ({ get }) => get(cartAtom).reduce((sum, i) => sum + i.price * i.qty, 0),
});
 
function Total() {
const total = useAtomValue(cartTotal); // re-renders only when the cart changes
return <strong>${total.toFixed(2)}</strong>;
}

Defining a selector

1
2
3
4
selector({
key: "unique:key",
get: ({ get }) => /* compute from get(otherAtomOrSelector) */,
});
  • key , a unique string (its own namespace from atoms is good practice).
  • get(api) , a pure function. It receives an object with a get function; call get(x) to read another

atom or selector. Every atom/selector you read this way becomes a tracked dependency.

Selectors are read-only: you cannot set a selector or pass one to useSetAtom. To "change" derived state, change the atoms it derives from.

Automatic dependency tracking

You do not declare dependencies; Mimir records them as your get runs. When you read get(cartAtom) inside the selector, cartAtom is added to the selector's dependency set. Later, when cartAtom changes, Mimir recomputes the selector and notifies its subscribers, but not when an unrelated atom changes.

This means a component reading cartTotal re-renders exactly when the cart changes, and a component reading the theme re-renders exactly when the theme changes, with no manual wiring.

Selectors can depend on selectors

A selector may read another selector. Mimir propagates the transitive leaf-atom dependencies, so a change to a deep underlying atom still notifies the top selector:

1
2
3
const subtotal = selector({ key: "cart:subtotal", get: ({ get }) => /* from cartAtom */ });
const taxed = selector({ key: "cart:taxed", get: ({ get }) => get(subtotal) * 1.2 });
// changing cartAtom recomputes subtotal AND taxed, and notifies subscribers of either

Cycle protection

If a selector reads itself, directly or through a chain, it would recurse forever and crash the process. Mimir detects re-entry and throws a clean error instead:

Mimir: circular selector dependency at 'cart:taxed'

If you see this, you have an accidental loop (selector A reads B which reads A). Break the cycle.

Reading selectors

  • In components: useAtomValue(mySelector) (subscribes; re-renders on dependency change).
  • Outside React: mimir.get(mySelector) (computes the current value).

useAtom's tuple form is not for selectors (there is no setter); use useAtomValue.

Selectors are computed, not stored

A selector's value is computed on read from the current atom values; it is not a separate stored slot you can mutate. This keeps derived state always-consistent: there is no way for cartTotal to drift out of sync with cartAtom, because it is recomputed from it.

When to use a selector vs computing in the component

  • Use a selector when the derivation is shared by several components, or feeds another selector, or you

want the dependency-scoped re-render behaviour (only recompute when inputs change).

  • Compute inline in a component when the derivation is local and cheap, const total = items.reduce(...)

in the component reading items is perfectly fine for a one-off.

Selectors shine for cross-cutting derived values: a cart total used in the header and the checkout, a "filtered, sorted list" several views render, a "is the form valid" flag many fields depend on.

Next: reacting to changes outside React, Subscriptions.