Browse documentation

action()

Factory for creating actions with access to state and context.

import { action } from '@comwit/state'

Usage

import { action, OnError, OnSuccess } from '@comwit/state'
import { post } from './model'
import { user } from '@/state/user/model'
import type { PostActions, AppContext } from './types'

export const postActions = action<Pick<PostActions, 'init' | 'loadPosts' | 'create'>, AppContext>(
  ({ state, context }) => {
    class PostActions {
      private model = state(post)
      private user = state(user) // cross-domain read

      init(data: Post) {
        this.model.current = data
      }

      async loadPosts() {
        await this.model.posts.query()
      }

      @OnSuccess(() => context.router.push('/posts'))
      @OnError((e) => toast.error(e instanceof Error ? e.message : 'Failed'))
      async create(title: string) {
        if (!this.user.me) return
        const created = await api.post.create({ userId: this.user.me.id, title })
        this.model.posts.data.push(created)
      }
    }
    return new PostActions()
  }
)

Do not call a mutating action directly during render. Await server-owned data in a Server Component and initialize its query entry with useDomain.hydrate(...) in the client route adapter. Actions remain the right place for user events, imperative preloads, and coordinated workflows.

Factory parameters

The factory receives { state, context }:

  • state(model) — returns a mutable proxy to a model's state. Capture it once for each model in the factory or a class field, then reuse that reference in methods.
  • context — the shared context from ComwitProvider (router, auth, etc.)

When a model has history: true, each action method call is recorded as one history transaction. Multiple state mutations inside the method are undone together via state.$history.undo().

Class pattern

A class inside the factory gives you:

  • Private fields for model references (this.model, this.user)
  • Decorator support (@OnError, @OnSuccess, @Debounce, etc.)
  • Clean method signatures that match your Actions type

Plain object methods are also supported when you do not need decorators:

const counterActions = action<CounterActions>(({ state }) => {
  const m = state(counter)
  return {
    increment() {
      m.count += 1
    },
  }
})

Both forms expose bound methods through the hook. Mutate the proxy returned by state(model); component selectors are for reading snapshots.

Cross-domain state

Actions can read and write any model via state():

const postActions = action<Pick<PostActions, 'create'>, AppContext>(({ state }) => {
  class Actions {
    private post = state(post)
    private user = state(user) // different domain

    async create(title: string) {
      if (!this.user.me) return
      // ...
    }
  }
  return new Actions()
})

Splitting actions

Split actions across files by concern. Each file exports one action factory with Pick<Actions, ...>:

// actions/crud.ts
export const crudActions = action<Pick<PostActions, 'create' | 'update' | 'delete'>, AppContext>(...)

// actions/load.ts
export const loadActions = action<Pick<PostActions, 'loadPosts'>, AppContext>(...)

Combine them in create().