Stores
Global state management with reactive stores.
What is a Store?
A store is a self-contained unit of state, computed values, and actions. Instead of scattering signals across your codebase, you define everything in one place and use it from any component. Think of it as Zustand meets signals: define a store once, use it everywhere.
import { createStore, derived } from 'what-framework';
const useCounter = createStore({
// State — becomes signals automatically
count: 0,
// Computed — uses derived() to declare
doubled: derived(state => state.count * 2),
// Actions — methods that mutate state
increment() { this.count++; },
decrement() { this.count--; },
});
createStore returns a hook function. Call it in any component to get reactive access to state, computed values, and actions:
function Counter() {
const store = useCounter();
return h('div', null,
h('p', null, 'Count: ', () => store.count),
h('p', null, 'Doubled: ', () => store.doubled),
h('button', { onClick: store.increment }, '+'),
h('button', { onClick: store.decrement }, '-'),
);
}
State Properties
Any non-function value in the store definition becomes a signal automatically. Numbers, strings, booleans, arrays, and objects all work:
const useSettings = createStore({
// All of these become signals
theme: 'dark', // string
fontSize: 16, // number
sidebarOpen: true, // boolean
tags: ['js', 'what'], // array
user: { name: 'Alice' }, // object
toggleSidebar() {
this.sidebarOpen = !this.sidebarOpen;
},
});
When you read a state property from the store object returned by the hook, you get the current value directly. No need to call it as a function. The getter on the returned object calls the underlying signal for you.
Derived Values
Use derived(fn) to declare computed properties that automatically update when the state they depend on changes:
import { createStore, derived } from 'what-framework';
const useCart = createStore({
items: [],
// Recomputes when items changes
totalItems: derived(state => state.items.length),
// Can depend on other state
totalPrice: derived(state =>
state.items.reduce((sum, item) => sum + item.price * item.qty, 0)
),
// Can format derived data
formattedTotal: derived(state =>
`$${state.items.reduce((s, i) => s + i.price * i.qty, 0).toFixed(2)}`
),
addItem(item) {
this.items = [...this.items, item];
},
});
The state parameter in the derived callback is a proxy that reads the current value of other store signals. Derived values are backed by computed() under the hood, so they are lazy and cached: they do not recompute until something they depend on actually changes.
Actions
Any function in the store definition that is not wrapped with derived() becomes an action. Inside actions, this is a proxy that lets you read and write state properties directly:
const useTodos = createStore({
items: [],
filter: 'all',
// Read state with this.items, write with this.items = ...
addTodo(text) {
this.items = [...this.items, {
id: Date.now(),
text,
done: false,
}];
},
toggleTodo(id) {
this.items = this.items.map(item =>
item.id === id ? { ...item, done: !item.done } : item
);
},
setFilter(filter) {
this.filter = filter;
},
// Actions can call other actions
clearCompleted() {
this.items = this.items.filter(item => !item.done);
},
});
All mutations inside a single action are automatically batched. If an action updates three signals, subscribers only re-render once after the action completes.
Using Stores in Components
The hook returned by createStore can be called in any component. Every component that calls the same hook shares the same underlying state:
function TodoList() {
const todos = useTodos();
return h('div', null,
h('h2', null, () => `${todos.items.length} items`),
h('ul', null,
() => todos.items.map(item =>
h('li', {
key: item.id,
onClick: () => todos.toggleTodo(item.id),
style: { textDecoration: item.done ? 'line-through' : 'none' },
}, item.text)
)
),
);
}
function AddTodo() {
const todos = useTodos();
return h('form', {
onSubmit: (e) => {
e.preventDefault();
const input = e.target.elements.text;
todos.addTodo(input.value);
input.value = '';
},
},
h('input', { name: 'text', placeholder: 'New todo...' }),
h('button', { type: 'submit' }, 'Add'),
);
}
Both TodoList and AddTodo share the same store instance. When AddTodo calls addTodo(), the list in TodoList updates automatically.
Multiple Stores
Organize your application state by feature. Each store is independent and focused on a single concern:
// stores/auth.js
export const useAuth = createStore({
user: null,
token: null,
isLoggedIn: derived(state => state.user !== null),
login(user, token) {
this.user = user;
this.token = token;
},
logout() {
this.user = null;
this.token = null;
},
});
// stores/cart.js
export const useCart = createStore({
items: [],
totalPrice: derived(state =>
state.items.reduce((s, i) => s + i.price * i.qty, 0)
),
addItem(product) { /* ... */ },
removeItem(id) { /* ... */ },
});
// stores/ui.js
export const useUI = createStore({
theme: 'light',
sidebarOpen: false,
modal: null,
toggleTheme() {
this.theme = this.theme === 'light' ? 'dark' : 'light';
},
openModal(name) { this.modal = name; },
closeModal() { this.modal = null; },
});
Stores vs Context
Use stores for global, application-wide state that many unrelated components need (auth, cart, theme). Use context for scoped state that only a subtree of components needs (form state, layout config). Stores are singletons; context can have multiple instances in different parts of the tree.
How It Works
Under the hood, createStore does three things:
- State values are converted into
signal()instances. Reading them creates reactive subscriptions. - Derived values (wrapped with
derived()) becomecomputed()instances. They track which state signals they read and recompute only when those change. - Actions are wrapped in
batch(). Thethiskeyword inside actions is a proxy that reads and writes the underlying signals. Writingthis.count++actually callscountSignal.set(countSignal.peek() + 1).
The hook function returned by createStore creates a new proxy object each time it is called, but all proxies point to the same underlying signals. This means every component gets fresh getter bindings while sharing the same reactive state.