A zustand-shaped reactive store for Ilha islands. Backed by alien-signals — the same engine that powers ilha core state — for shared global state that lives outside any single island.
bun add @ilha/storeimport { createStore } from "@ilha/store";
const store = createStore({ count: 0 });
store.setState({ count: 1 });
store.getState(); // → { count: 1 }ilha state is island-local — signals are scoped to a single component instance. Use @ilha/store when you need state that is:
- Shared across multiple islands — e.g. a cart, auth session, or theme
- Updated from outside an island — e.g. from a WebSocket handler or a global event bus
- Persisted or derived globally — e.g. synced to
localStoragevia asubscribelistener
For state that only one island reads and writes, prefer ilha's built-in .state().
Creates a store. Optionally accepts an actions creator for encapsulating state mutations.
// State only
const store = createStore({ count: 0, name: "Ada" });
// State + actions
const store = createStore({ count: 0 }, (set, get) => ({
increment() {
set({ count: get().count + 1 });
},
reset() {
set({ count: 0 });
},
}));
store.getState().increment();
store.getState().count; // → 1The actions creator receives:
| Argument | Description |
|---|---|
set(patch | updater) |
Merge a partial patch or apply an updater function |
get() |
Read the current live state (includes other actions) |
getInitialState() |
Read the frozen initial state snapshot |
Merges a partial state update. Accepts a plain object or an updater function.
store.setState({ count: 5 });
store.setState((s) => ({ count: s.count + 1 }));Returns the current state snapshot.
store.getState(); // → { count: 5 }Returns the frozen initial state as it was at construction time.
store.getInitialState(); // → { count: 0 }Subscribes to all state changes. The listener receives the next and previous state. Returns an unsubscribe function.
const unsub = store.subscribe((state, prev) => {
console.log(state.count, prev.count);
});
unsub(); // stop listeningSubscribes to a derived slice. The listener only fires when the selected value changes (compared with Object.is).
const unsub = store.subscribe(
(s) => s.count,
(count, prev) => console.log("count changed:", prev, "→", count),
);Reactively renders a store-driven HTML string into a DOM element whenever state changes. The render function may return a plain string or an html\`` tagged template.
import { html } from "@ilha/store";
const unsub = store.bind(
document.getElementById("counter")!,
(state) => html`<p>Count: ${state.count}</p>`,
);
unsub(); // detachOnly re-renders when the selected slice changes.
store.bind(
document.getElementById("badge")!,
(s) => s.count,
(count) => html`<span>${count}</span>`,
);The most common pattern is reading the store inside an island's .effect() and calling store.subscribe() to drive reactive re-renders:
import { createStore, html } from "@ilha/store";
import ilha from "ilha";
export const cartStore = createStore({ items: [] as string[] }, (set, get) => ({
add(item: string) {
set({ items: [...get().items, item] });
},
remove(item: string) {
set({ items: get().items.filter((i) => i !== item) });
},
}));
export const CartIsland = ilha
.state("items", cartStore.getState().items)
.effect(({ state }) => {
return cartStore.subscribe(
(s) => s.items,
(items) => state.items(items),
);
})
.render(
({ state }) => html`
<ul>
${state.items().map((item) => html`<li>${item}</li>`)}
</ul>
`,
);Key exported types:
import type {
StoreApi, // the store instance interface
SetState, // (patch | updater) => void
GetState, // () => T
Listener, // (state, prevState) => void
SliceListener, // (slice, prevSlice) => void
RenderResult, // string | RawHtml
Unsub, // () => void
} from "@ilha/store";MIT