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.

PropPurpose
contextShared values available to action factories, such as the router or auth helpers
getServerSearchParamsOptional () => string | null server getter for searchParam() fields
useServerInsertedHTMLOptional framework hook that streams server-resolved .suspend() results (experimental)
defaultOptions.queryGlobal staleTime, gcTime, and placeholderData
defaultOptions.persistGlobal persistence debounce interval
defaultOptions.localIndexedDB database, fallback scope, and storage error handler
defaultOptions.interceptorsInterceptors 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:

  • getServerSearchParams runs once during server initialization and seeds searchParam() fields. The provider transfers the result to hydration as inert JSON; the browser never calls it.
  • useServerInsertedHTML from next/navigation inserts an inert JSON script with the .suspend() results resolved so far ahead of each flushed chunk. Hydrating selectors read those scripts instead of running queryFn again. 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.