Overview

store is useState at module scope: call store(initialObject) once to create a shared reactive bag, then call the returned hook inside any component to get [wrapper, update].

Every component that reads a property of the wrapper subscribes to re-render when that state changes. Methods, direct assignment, and update() follow the same rules as useState — mutations from one component are visible in all others immediately.

Keep stores domain-focused (authStore, cartStore, uiStore). For parent→child UI plumbing without globals, prefer scope() + getParentScope().

Compared to useState

API
Scope
Notes
useState(obj) component-local Each component instance gets its own independent state.
store(obj) module-global One shared object; the factory returns a hook for any component.

store — shared global state

Both Counter and StatsPanel call counterStore() independently, yet they share the exact same state. A mutation from Counter is instantly visible in StatsPanel — no props or context plumbing required.

import { html, store, x, renderApp } from 'mates';

// Created at MODULE LEVEL — shared across all components
const counterStore = store({
count: 0,
step: 1,
increment() { this.count += this.step; },
decrement() { this.count -= this.step; },
reset() { this.count = 0; },
get doubled() { return this.count * 2; },
});

// Two separate components sharing the same store
const Counter = () => {
const [state] = counterStore();
return () => html`
<div class="card card-body m-col m-gap center">
<span class="label">Counter A</span>
<h1>${state.count}</h1>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-danger" @click=${state.decrement}>−</button>
<button @click=${state.reset}>Reset</button>
<button class="btn-primary" @click=${state.increment}>+</button>
</div>
</div>
`;
};

const StatsPanel = () => {
const [state] = counterStore();
return () => html`
<div class="m-grid m-grid-cols-3 m-gap">
<div class="card card-body center"><span class="overline">Count</span><span class="card-title">${state.count}</span></div>
<div class="card card-body center"><span class="overline">Doubled</span><span class="card-title">${state.doubled}</span></div>
<div class="card card-body center"><span class="overline">Step</span><span class="card-title">${state.step}</span></div>
</div>

Methods and update()

Auto-wrapped methods mutate shared prefs; update(fn) batches structural edits in one notify — same API as useState, shared across the app.

import { html, store, renderApp } from 'mates';

const prefsStore = store({
fontScale: 1,
dense: false,
bump() { this.fontScale = Math.min(2, this.fontScale + 0.25); },
shrink() { this.fontScale = Math.max(0.75, this.fontScale - 0.25); },
});

const App = () => {
const [prefs, update] = prefsStore();

return () => html`
<div class="m-col m-gap">
<p class="muted" style="font-size:${prefs.fontScale}rem">
Shared prefs — scale ${prefs.fontScale.toFixed(2)}
</p>
<p class="muted m-t-5">dense=${String(prefs.dense)}</p>
<div class="m-flex m-items-center m-justify-center m-gap-sm">
<button class="btn-primary" @click=${prefs.bump}>A+</button>
<button @click=${prefs.shrink}>A−</button>
<button class="btn-ghost" @click=${() => {
update((s) => { s.dense = !s.dense; s.fontScale = 1; });
}}>update() toggle</button>
</div>
</div>
`;
};
renderApp(App, document.getElementById('app'));