Quickstart
Install, define a domain, and use its hook. These examples work with React 18 or 19; the provider placement below uses the Next.js App Router.
1. Install
npm i @comwit/state
If you're building with an agent, give it llms.txt. It contains the core setup and
usage contract. You can also follow the steps below yourself.
2. Add the provider
// app/providers.tsx
'use client'
import { ComwitProvider } from '@comwit/state'
import type { ReactNode } from 'react'
export function Providers({ children }: { children: ReactNode }) {
return (
<ComwitProvider defaultOptions={{ query: { staleTime: 30_000 } }}>{children}</ComwitProvider>
)
}
// app/layout.tsx
import type { ReactNode } from 'react'
import { Providers } from './providers'
export default function RootLayout({ children }: { children: ReactNode }) {
return (
<html lang="en">
<body>
<Providers>{children}</Providers>
</body>
</html>
)
}
In a React app, wrap your root component with ComwitProvider instead. Shared context, persistence,
and global interceptors are optional; see Provider.
3. Define a domain
Start with a small counter. The state contract and action contract tell both TypeScript and your agent what the feature exposes.
// state/counter/types.ts
export type CounterState = { count: number }
export type CounterActions = { increment(): void; reset(): void }
// state/counter/model.ts
import { model } from '@comwit/state'
import type { CounterState } from './types'
export const counter = model<CounterState>({ count: 0 })
// state/counter/actions/interact.ts
import { action } from '@comwit/state'
import { counter } from '../model'
import type { CounterActions } from '../types'
export const counterActions = action<CounterActions>(({ state }) => {
class Actions {
private counter = state(counter)
increment() {
this.counter.count += 1
}
reset() {
this.counter.count = 0
}
}
return new Actions()
})
// state/counter/index.ts
import { create } from '@comwit/state'
import { counter } from './model'
import { counterActions } from './actions/interact'
import type { CounterState, CounterActions } from './types'
export const useCounter = create<CounterState, CounterActions>(counter, {
actions: [counterActions],
})
4. Read state, call actions
// app/page.tsx
'use client'
import { useCounter } from '@/state/counter'
export default function CounterPage() {
const { count, actions } = useCounter((s) => ({
count: s.count,
actions: s.actions,
}))
return (
<main>
<p>Count: {count}</p>
<button onClick={actions.increment}>Add one</button>
<button onClick={actions.reset}>Reset</button>
</main>
)
}
The action changes the object. The selector subscribes to count. Each provider owns an independent
counter, so separate app roots and server requests do not share a singleton state instance.
5. Add server data when you need it
Use a query field and call .load() from the view that owns the request. Here /api/products is
your app's endpoint returning Product[]:
// state/product/index.ts — a compact domain; split files as it grows
import { create, model, query } from '@comwit/state'
type Product = { id: string; title: string }
const product = model({
list: query<Product[]>({
initialData: [],
queryFn: async () => {
const response = await fetch('/api/products')
if (!response.ok) throw new Error('Could not load products')
return (await response.json()) as Product[]
},
}),
})
export const useProduct = create(product, { actions: [] })
'use client'
import { useProduct } from '@/state/product'
export function ProductList() {
const list = useProduct((s) => s.list.load())
if (list.isLoading) return <p>Loading products…</p>
if (list.isError && !list.isSuccess) return <p>{list.error}</p>
if (list.data.length === 0) return <p>No products yet.</p>
return (
<ul aria-busy={list.isFetching}>
{list.data.map((product) => (
<li key={product.id}>{product.title}</li>
))}
</ul>
)
}
.load() returns loading state during render and starts the request after commit. Selecting
s.list alone is a passive read. For data already fetched by a Server Component, follow the
Next.js guide and use useProduct.hydrate(...).
Next steps
- Project Structure — organize domains and their public contracts.
- query() — arguments, pagination, streaming, and subscriptions.
- local() — restore normalized IndexedDB data before revalidation.
Migrating from comwit? Version 2 uses the @comwit/state package name. See the
v2.0.0 release notes.