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
Actionstype
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().