State Management
Whisq doesn’t need a state management library. Shared state is just signals and functions exported from a module. Any component that imports them stays in sync automatically.
The Store Pattern
Section titled “The Store Pattern”Create a file that exports signals and functions:
import { signal, computed } from "@whisq/core";
export const count = signal(0);export const doubled = computed(() => count.value * 2);export const increment = () => { count.value++; };export const decrement = () => { count.value--; };export const reset = () => { count.value = 0; };Use it in any component:
import { component, div, button, span } from "@whisq/core";import { count, doubled, increment, decrement } from "./stores/counter";
const Display = component(() => div( span(() => `Count: ${count.value}`), span(() => ` (doubled: ${doubled.value})`), ));
const Controls = component(() => div( button({ onclick: decrement }, "-"), button({ onclick: increment }, "+"), ));Both components share the same state. When increment() is called in Controls, the Display updates automatically.
A Real-World Store
Section titled “A Real-World Store”A todo store with add, remove, toggle, and derived state:
import { signal, computed } from "@whisq/core";
interface Todo { id: number; text: string; done: boolean;}
let nextId = 1;
export const todos = signal<Todo[]>([]);export const remaining = computed(() => todos.value.filter(t => !t.done).length);
export const addTodo = (text: string) => { todos.value = [...todos.value, { id: nextId++, text, done: false }];};
export const toggleTodo = (id: number) => { todos.value = todos.value.map(t => t.id === id ? { ...t, done: !t.done } : t );};
export const removeTodo = (id: number) => { todos.value = todos.value.filter(t => t.id !== id);};Local vs Shared State
Section titled “Local vs Shared State”Local state lives inside a component. Use it for UI state that only that component needs:
import { signal, component, div, button, when } from "@whisq/core";
const Accordion = component(() => { const open = signal(false);
return div( button({ onclick: () => open.value = !open.value }, "Toggle"), when(() => open.value, () => div("Content here")), );});Shared state lives in a store module. Use it when multiple components need the same data:
import { signal, computed } from "@whisq/core";
export const user = signal<{ name: string } | null>(null);export const isLoggedIn = computed(() => !!user.value);export const logout = () => { user.value = null; };Batch Updates
Section titled “Batch Updates”When updating multiple signals at once, wrap them in batch() to avoid intermediate renders:
import { signal, batch } from "@whisq/core";
const firstName = signal("");const lastName = signal("");const age = signal(0);
const loadProfile = (profile: { first: string; last: string; age: number }) => { batch(() => { firstName.value = profile.first; lastName.value = profile.last; age.value = profile.age; }); // UI updates once, not three times};Composing Stores
Section titled “Composing Stores”Stores can import from other stores:
import { signal, computed } from "@whisq/core";import { user } from "./auth";
export const items = signal<{ name: string; price: number; qty: number }[]>([]);export const total = computed(() => items.value.reduce((sum, item) => sum + item.price * item.qty, 0));export const canCheckout = computed(() => !!user.value && items.value.length > 0);Next Steps
Section titled “Next Steps”- Signals — How reactivity works under the hood
- Components — Building component trees
- Data Fetching — Loading server data into stores