Skip to content

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.

Create a file that exports signals and functions:

stores/counter.ts
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 todo store with add, remove, toggle, and derived state:

stores/todos.ts
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 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:

stores/auth.ts
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; };

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
};

Stores can import from other stores:

stores/cart.ts
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
);