---
title: Derived state and actions
description: computed values, named mutations, and middleware for @ilha/store.
---

Chain `.derived()`, `.action()`, and `.middleware()` onto a [store builder](/guide/store/overview) 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.

```ts twoslash
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 derived** — `fn` can be `async`. It re-runs when its `get()` dependencies change, aborts stale runs via `signal`, and surfaces the async lifecycle:

```ts twoslash
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()`](/guide/store/persistence-and-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.

```ts
// 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:

```ts
.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):

```tsx
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.

```ts twoslash
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.

## Related

| Topic                        | Guide                                                                     |
| ---------------------------- | ------------------------------------------------------------------------- |
| Subscriptions and validation | [Subscriptions and validation](/guide/store/subscriptions-and-validation) |
| Data-fetching query cache    | [Persistence and query](/guide/store/persistence-and-query)               |
| Building a store             | [Store overview](/guide/store/overview)                                   |
