ComwitProvider
Wrap the client tree once. The provider owns the model instances and query cache; each model initializes when a hook or action first accesses it.
'use client'
import { ComwitProvider, keepPreviousData } from '@comwit/state'
import { useRouter, useServerInsertedHTML } from 'next/navigation'
import type { ReactNode } from 'react'
import { getServerSearchParams } from './get-server-search-params'
export function Providers({ children }: { children: ReactNode }) {
const router = useRouter()
return (
<ComwitProvider
context={{ router }}
getServerSearchParams={getServerSearchParams}
useServerInsertedHTML={useServerInsertedHTML}
defaultOptions={{
query: {
staleTime: 30_000,
gcTime: 5 * 60_000,
placeholderData: keepPreviousData,
},
persist: { debounceMs: 100 },
local: { database: 'my-app' },
}}
>
{children}
</ComwitProvider>
)
}
Only children is required. <ComwitProvider>{children}</ComwitProvider> is a complete setup.
| Prop | Purpose |
|---|---|
context | Shared values available to action factories, such as the router or auth helpers |
getServerSearchParams | Optional () => string | null server getter for searchParam() fields |
useServerInsertedHTML | Optional framework hook that streams server-resolved .suspend() results (experimental) |
defaultOptions.query | Global staleTime, gcTime, and placeholderData |
defaultOptions.persist | Global persistence debounce interval |
defaultOptions.local | IndexedDB database, fallback scope, and storage error handler |
defaultOptions.interceptors | Interceptors applied to every action method |
Query defaults can be overridden on a query field or by an imperative .query(arg, options) call.
Selector .load(arg) only takes the query argument.
Framework functions
The provider never imports a framework. Where Next.js owns request data or the HTML stream, pass the function it exports and the provider decides when to call it:
getServerSearchParamsruns once during server initialization and seedssearchParam()fields. The provider transfers the result to hydration as inert JSON; the browser never calls it.useServerInsertedHTMLfromnext/navigationinserts an inert JSON script with the.suspend()results resolved so far ahead of each flushed chunk. Hydrating selectors read those scripts instead of runningqueryFnagain. It is a hook, so keep it fixed for the provider's lifetime.
Both props are optional and independent. See the Next.js guide for the
complete providers.tsx and the request getter.
Use shared context in actions
import { action } from '@comwit/state'
type AppContext = { router: { push(href: string): void } }
type NavigationActions = { openProducts(): void }
export const navigationActions = action<NavigationActions, AppContext>(({ context }) => ({
openProducts() {
context.router.push('/products')
},
}))
Context values update with provider renders; read them when the action runs when they may change over time.
Global interceptors
import { ComwitProvider, Log, OnError } from '@comwit/state'
;<ComwitProvider
defaultOptions={{
interceptors: [OnError((error) => reportError(error)), Log('info')],
}}
>
{children}
</ComwitProvider>
Execution order is provider → class → method → action body. See Decorators for retries, authorization, queues, and validation.
Isolation and local storage scopes
Nested providers own independent state. A new provider starts a new in-memory registry. For provider-level user or tenant scopes, remount the provider when the scope changes:
<ComwitProvider key={user.id} defaultOptions={{ local: { scope: `user:${user.id}` } }}>
{children}
</ComwitProvider>
A collection can instead define its own static or model-derived scope, which takes precedence over the provider fallback. See local() scope rules.