Skip to content
Ilha
Esc
navigateopen⌘Jpreview
On this page

Derived state and actions

computed values, named mutations, and middleware for @ilha/store.

Chain .derived(), .action(), and .middleware() onto a store builder before calling .build(). Builder methods are immutable — each returns a new StoreBuilder.

.derived(key, fn)

Registers a computed value. Destructure get from the callback context to read the current raw state.

import { store } from "@ilha/store";

store({ count: 0 })
  .derived("doubled", ({ get }) => get().count * 2)
  .build();

Derived accessors expose an envelope — () / .value, .loading, .error. For sync deriveds, .loading is always false.

Async derivedfn can be async. It re-runs when its get() dependencies change, aborts stale runs via signal, and surfaces the async lifecycle:

import { store } from "@ilha/store";

const userStore = store({ id: 1 })
  .derived("user", async ({ get, signal }) => {
    const res = await fetch(`/api/users/${get().id}`, {
      signal,
    });
    return res.json();
  })
  .build();

userStore.user.loading; // true while fetching
userStore.user(); // User | undefined
userStore.user.error; // Error | undefined on rejection

Re-running (when id changes) keeps the previous .value visible while .loading is true.

For cache, dedup, and cross-store sharing of the same fetch, wrap the work in query() from @ilha/store/query.

Sync deriveds are evaluated once eagerly at .build() (to detect promise-returning functions); async deriveds start their first run at .build() too. Keep derived functions cheap and side-effect-free — don’t assume they only run when read.

.action(key, fn)

Registers a named mutation. fn may be sync or async. Return a Partial patch (or Promise of one) to merge through middleware, or void when writes use ctx.set only or you only run side effects.

// Zero-arg — omit or leave first param unannotated
.action("increment", (_, { get }) => ({ count: get().count + 1 }))

// Typed props — annotate the first parameter
.action("setLabel", (label: string) => ({ label }))

The context exposes { get(), getInitial(), set(patch) }. Destructure the members you use. Use set for async or multi-step actions:

.action("load", (_, { get, set }) => {
  set({ loading: true });
  fetchUser(get().id).then((user) => set({ user, loading: false }));
})

Every action exposes a reactive .pending flag — true while any async invocation is in flight (sync actions never set it):

ilha(() => (
  <button disabled={s.save.pending}>
    {s.save.pending ? "Saving…" : "Save"}
  </button>
));

.middleware(fn)

Intercepts every state mutation before it commits. Receives the patch, context, and next. Destructure the context members you use. Call next(patch) to continue or return early to block. Applies to accessor writes, setState, actions, and bind writes.

import { store } from "@ilha/store";

store({ count: 0 }).middleware((patch, { get }, next) => {
  console.log("before:", get().count);
  next(patch);
});

Multiple middlewares compose in registration order.

Topic Guide
Subscriptions and validation Subscriptions and validation
Data-fetching query cache Persistence and query
Building a store Store overview

Was this page helpful?