Stores
There are two ways to share state:
- Parent state is visible to nested components automatically. See Nested components and scope. This covers most sharing inside one part of a page.
- A store holds app-wide state that several unrelated components use, or that should outlive any one component.
defineStore
// /state/cart.js
import { defineStore } from "@realmorg/realm";
export const cart = defineStore(
"cart",
{
items: [],
get count() {
return this.items.reduce((sum, item) => sum + item.quantity, 0);
},
},
{ persist: "local" },
);<custom-element name="cart-badge">
<script type="module">
import { cart } from "/state/cart.js";
</script>
<ui-badge><slot name="#cart.count">0</slot></ui-badge>
</custom-element>
<custom-element name="add-to-cart">
<script type="module">
import { cart } from "/state/cart.js";
const props = defineProps({ sku: prop.string().required() });
function add() {
const line = cart.items.find((item) => item.sku === props.sku);
if (line) line.quantity++;
else cart.items.push({ sku: props.sku, quantity: 1 });
}
</script>
<ui-button onclick="add()">Add to cart</ui-button>
</custom-element>- A store is reactive like
useState, including getters. defineStorereturns the same store for the same name, wherever it is called. Defining it in a module and importing it is the clearest pattern. CallingdefineStore("cart", …)directly in two component scripts also gives both the same store.- Nothing is attached to
window.
defineStore is also available inside component scripts without importing, which is handy for small apps:
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Stores demo</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.css">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@realmorg/ui@alpha/dist/theme.css">
<script type="module" src="https://cdn.jsdelivr.net/npm/@realmorg/realm@alpha/dist/realm.js"></script>
</head>
<body>
<import-element from="./theme-picker.html"></import-element>
<import-element from="./accent-preview.html"></import-element>
<theme-picker></theme-picker>
<accent-preview></accent-preview>
</body>
</html><custom-element name="theme-picker">
<script>
const settings = defineStore("docs-demo-settings", { accent: "chart-1" }, { persist: "session" });
const ACCENTS = ["chart-1", "chart-2", "chart-3", "chart-4", "chart-5"];
const picker = useState({
get swatches() {
return ACCENTS.map((id) => ({ id, color: `var(--${id})`, current: id === settings.accent }));
},
});
function choose(swatch) {
settings.accent = swatch.id;
}
</script>
<style>
:host {
display: flex;
gap: calc(var(--spacing) * 2);
}
button {
width: calc(var(--spacing) * 8);
height: calc(var(--spacing) * 8);
border: 2px solid transparent;
border-radius: var(--radius-full);
background: var(--swatch);
cursor: pointer;
}
button[aria-pressed="true"] {
border-color: var(--foreground);
}
</style>
<button type="button" repeat="#picker.swatches" as="swatch" key="id" _aria-label="#swatch.id" _aria-pressed="#swatch.current" _--swatch="#swatch.color" onclick="choose(swatch)"></button>
</custom-element><custom-element name="accent-preview">
<script>
const settings = defineStore("docs-demo-settings", { accent: "chart-1" }, { persist: "session" });
const preview = useState({
get color() {
return `var(--${settings.accent})`;
},
});
</script>
<style>
:host {
display: block;
padding: calc(var(--spacing) * 3) calc(var(--spacing) * 4);
border-radius: var(--radius-md);
background: var(--state-preview-color);
color: var(--primary-foreground);
font-weight: var(--font-weight-medium);
}
</style>
Accent: <slot name="#settings.accent"></slot>
</custom-element>Both components call defineStore with the same name, so they share one store. The choice survives a reload in this tab because the store persists to sessionStorage.
Persistence
persist |
Where values are saved |
|---|---|
"local" |
localStorage, kept across sessions |
"session" |
sessionStorage, kept for the tab |
| An adapter | Anything with get(key) and set(key, value) |
- Values are saved as JSON under the key
realm:<name>after every change, and restored when the store is defined. - Types are kept: numbers, booleans, arrays and nested objects come back as they were.
- Getters are never saved or overwritten.
- A stored value that is not valid JSON is ignored with a warning, and the initial values are used.
A custom adapter, for example to namespace keys or add expiry:
const adapter = {
get: (key) => localStorage.getItem(`myapp:${key}`),
set: (key, value) => localStorage.setItem(`myapp:${key}`, value),
};
export const prefs = defineStore("prefs", { density: "comfortable" }, { persist: adapter });Things to know
- A store is defined once per name. Later calls return the existing store and ignore their arguments, so define it in one place.
- Stores are never torn down. Observers you create on a store from inside a component still stop with the component.
- Keep stores for shared data. A value only one component uses belongs in that component's
useState.