npm install @comwit/state
v2.5.0 brings the 2.5 beta line to the stable @comwit/state channel. Every API from 2.4.0 keeps
its behavior and there is no migration step. From this release on there is no separate beta
channel: the next features ship as regular minor releases.
A Suspense boundary that hydrates with its data
.suspend(arg) runs a query during render and throws the pending Promise, so a <Suspense>
boundary streams its fallback and then its content. In 2.4 the browser had a separate cache and ran
queryFn again while hydrating. Now the provider carries each result resolved on the server into
the browser cache, so the boundary hydrates with the same data and no second request.
Pass the Next.js hook to the provider. Nothing else changes:
'use client'
import { ComwitProvider } from '@comwit/state'
import { useRouter, useServerInsertedHTML } from 'next/navigation'
import { getServerSearchParams } from './get-server-search-params'
export function Providers({ children }: { children: React.ReactNode }) {
const router = useRouter()
return (
<ComwitProvider
context={{ router }}
getServerSearchParams={getServerSearchParams}
useServerInsertedHTML={useServerInsertedHTML}
>
{children}
</ComwitProvider>
)
}
// Server Component
export default function ProductPage({ id }: { id: string }) {
return (
<Suspense fallback={<ProductSkeleton />}>
<ProductDetail id={id} />
</Suspense>
)
}
// Client Component
function ProductDetail({ id }: { id: string }) {
const product = useProduct((s) => s.detail.suspend(id))
return product.data ? <ProductView product={product.data} /> : <NotFound />
}
server: suspend(id) → queryFn → boundary HTML, preceded by an inert JSON script
browser: hydrating suspend(id) reads the script → same snapshot, no queryFn → active after commit
The framework function is injected, the same way as getServerSearchParams; there is no
Next.js-specific provider. On the server, useServerInsertedHTML inserts a
<script type="application/json" data-comwit-suspend> ahead of each flushed chunk with the results
resolved since the previous flush. In the browser the hydrating selector reads that script before
its first snapshot, the entry becomes active after commit with the server's fetch time, and
staleTime applies as if the browser had fetched it. Results are keyed by the selecting hook's tree
position, so nested providers and repeated components cannot cross-hydrate, and no executable inline
script is emitted.
The requirements are in the
Next.js guide: queryFn must run in
both environments (no Server Functions, absolute fetch URLs on the server), results must be
JSON-serializable, and hydrate() remains the default for data that must stay on the server.
The first .suspend() result activates the resource
A .suspend() result used to live only in the key cache until the selecting tree committed. On the
server there is no commit, so a sibling or nested component reading state.field.data rendered
initialData (a logged-out header next to a signed-in page) while the suspend caller rendered the
data, and the browser then reported hydration mismatches.
Since 2.5.0 the first resolved .suspend() result of a never-queried resource becomes the proxy
value immediately, the same "initialize before the first snapshot" rule hydrate() follows. The
streamed restore does the same during hydration, and useModel re-reads its snapshot so passive
fields in the same selector observe the restored value. A key staged over an already-active resource
still waits for commit, so an abandoned transition never replaces the visible screen.
Local queries survive a scope that resolves mid-request
A local.collection() scope can come from another model, for example the signed-in user. If a
query started while that scope was still unknown and the scope resolved before the response arrived,
the binding treated it as a switch to another identity and the query stayed isLoading: true.
Resolving an unknown scope now adopts it: the in-flight response finishes normally and is persisted
under the resolved scope. Switching between two resolved scopes still drops the late response and
resets the data, as before.
Compatibility
Without useServerInsertedHTML, .suspend() behaves as in 2.4: the server and browser caches stay
separate and the browser fetches again while hydrating. The deprecated silent() export is still
available for compatibility; use hydrate() or a streamed .suspend() for server data.