Patterns
Derived State
For computed values that depend on other state, use traps (transforms) on $ references. These re-evaluate automatically when the source data changes:
const state = createHook({
todos: [],
filter: "all",
});
// Derived values using traps
const filteredTodos = state.$filter((filter, snapshot) => {
if (filter === "all") return snapshot.todos;
return snapshot.todos.filter(t =>
filter === "active" ? !t.completed : t.completed,
);
});
const activeCount = state.$todos((todos) =>
todos.filter((t) => !t.completed).length,
);Use them directly in templates — they're reactive:
html`<span>${activeCount} items left</span>`;With method forwarding
Method calls on $ references are also reactive — they chain as transforms:
html`<span>${state.$todos.filter(t => !t.completed).length()} items left</span>`;Note that calling length() works because .length on a $ reference returns a callable, but this is fragile — prefer a trap for getter-only properties.
// ✅ Clear and reliable — use a trap
html`<span>${state.$todos((todos) => todos.filter(t => !t.completed).length)} items left</span>`;What NOT to do
JavaScript operators on $ references are evaluated immediately at render time and are not reactive:
// ❌ NOT reactive — evaluated once
state.$filter === "all" // returns true/false at render time, never updates
state.$todos.length > 0 ? html`...` : "" // ternary evaluates once
state.$todos.map(todo => html`<li>...</li>`) // .map evaluates onceInstead, use traps or move the logic into store properties updated with watch.
Trap Chaining
A trap returns a callable HookRef that can itself receive a transform, composing the two:
const state = createHook({ items: ["a", "b", "c"] });
const length = state.$items((items) => items.length);
const isLong = length((n) => n > 2);
// isLong resolves to true when items.length > 2
// Use in a template — reacts to source changes
html`<span :text=${isLong}></span>`;The second transform receives the result of the first, plus a plain snapshot of the hook state:
state.$items((items) => items.length)((n, snapshot) => n > 0);This is equivalent to composing the functions manually, but preserves reactivity through the pipeline.
Multiple State Slices
For larger apps, split state into separate hooks:
const userState = createHook({ name: "", preferences: {} });
const uiState = createHook({ theme: "light", sidebar: true });
const dataState = createHook({ items: [], loading: false });This keeps concerns separate and avoids deeply nested proxies.
Immutable Updates
Always replace arrays and objects rather than mutating them:
// ❌ Won't trigger reactivity
state.todos.push(newTodo);
// ✅ Triggers reactivity
state.todos = [...state.todos, newTodo];
// ❌ Won't trigger reactivity
state.user.name = "New Name";
// ✅ Triggers reactivity
state.user = { ...state.user, name: "New Name" };State Initialization
Use a factory function for state initialization (especially useful for localStorage persistence):
function createInitialState() {
return {
count: 0,
user: null,
items: [],
};
}
const state = createHook(createInitialState());Resetting State
Store the initial state to enable reset functionality:
const initial = { count: 0, name: "" };
const state = createHook({ ...initial });
function reset() {
state.count = initial.count;
state.name = initial.name;
}