SSR
Server-side rendering (SSR) with stores requires isolating state per request and dehydrating data for client hydration. The Dependency Injection system ensures that each request gets its own signal instances, preventing state leakage between users.
Marking Signals for Hydration
Section titled “Marking Signals for Hydration”Use hydratable to mark signals that should participate in dehydration/hydration. This assigns a unique key to each signal for later identification.
import { signal, hydratable, mountable } from '@nano_kit/store'
function User$() { const $userId = hydratable('userId', signal(null)) const $user = hydratable('user', signal(null))
return { $userId, $user }}On the server, hydratable registers the signal in an internal map with its key. On the client, if dehydrated data exists for that key, the signal is immediately initialized with the hydrated value instead of the default.
hydratable also accepts a Codec as the third argument. You only need it when the dehydrated payload must serialize values that need custom encoding, such as Date, Map, Set, class instances, or encrypted/compact payloads.
import type { Codec } from '@nano_kit/store'import { parse, stringify } from 'devalue'import { signal, hydratable } from '@nano_kit/store'
interface DashboardSnapshot { generatedAt: Date selectedUserIds: Set<number> usersById: Map<number, { name: string }>}
const DevalueCodec: Codec<unknown, string> = { encode: stringify, decode: value => value === null ? null : parse(value)}
function Dashboard$() { const $snapshot = hydratable( 'dashboard', signal<DashboardSnapshot | null>(null), DevalueCodec )
return { $snapshot }}On the server, the codec encodes the signal value before it is added to the dehydrated data. On the client, it decodes the dehydrated value before writing it into the signal.
Checking Hydration State
Section titled “Checking Hydration State”Use isHydrated to check whether a signal has been populated from hydration data. The hydrated flag is cleared on the signal’s first user-driven update.
import { isHydrated, onMountEffect } from '@nano_kit/store'
function User$() { /* ... */ onMountEffect($user, () => { /* Subscribe to userId changes */ const userId = $userId() /* Only fetch user data if it hasn't been hydrated from the server */ if (!isHydrated($user)) { fetchUser(userId) } }) /* ... */}Working with Tasks
Section titled “Working with Tasks”SSR requires waiting for all async operations to complete before dehydrating. Use TasksRunner$ to create a task runner that automatically tracks async operations in TasksPool$.
import { inject, signal, hydratable, mountable, onMountEffect, action, TasksRunner$ } from '@nano_kit/store'
function User$() { const task = inject(TasksRunner$) const $userId = hydratable('userId', signal(null)) const $user = hydratable('user', mountable(signal(null)))
const fetchUser = action((id) => task(async () => { if (typeof id !== 'number') { $user(null) return }
const response = await fetch(`/user/${id}`) const user = await response.json()
$user(user) }))
onMountEffect($user, () => { fetchUser($userId()) })
return { $userId, $user }}The task function wraps async operations and adds them to the tasks pool. The dehydrate function waits for all tasks in the pool to complete before extracting signal values.
Manual Hydration Flow
Section titled “Manual Hydration Flow”Server-Side Dehydration
Section titled “Server-Side Dehydration”On the server, use dehydrate to execute your store logic, wait for all async tasks to complete, and extract the dehydrated key-value pairs.
import { dehydrate, inject } from '@nano_kit/store'
/* Server-side handler */const dehydrated = await dehydrate(() => { const { $userId, $user } = inject(User$)
/* Set initial data */ $userId(1)
/* Return signals to trigger mount and start async operations */ return [$user]})
/* dehydrated = [['userId', 1], ['user', { name: 'John', email: '...' }]] */The dehydrate function:
- Creates an injection context with a task pool, or uses the context passed as the second argument
- Runs your store factories within that context
- Starts effects to trigger
onMountcallbacks (which start async tasks) - Waits for all tasks in
TasksPool$to complete - Collects all signals marked with
hydratable - Returns the dehydrated key-value pairs
If your stores need preconfigured dependencies, pass them as the second argument. It can be an InjectionContext instance or an array of providers for a new context.
import { dehydrate, provide } from '@nano_kit/store'
const dehydrated = await dehydrate(Stores$, [ provide(UserId$, 1)])Client-Side Hydration
Section titled “Client-Side Hydration”On the client, pass the dehydrated data to a StaticHydrator and provide it via the Hydrator$ injection token.
import { Hydrator$, StaticHydrator, provide } from '@nano_kit/store'import { InjectionContextProvider, useInject, useSignal } from '@nano_kit/react'
/* Dehydrated data passed from the server (e.g. via a script tag or window variable) */const hydrator = new StaticHydrator(window.__DEHYDRATED__)
/* Component that provides the injection context with hydration data */function App() { return ( <InjectionContextProvider context={[provide(Hydrator$, hydrator)]}> <UserProfile /> </InjectionContextProvider> )}
/* Component that uses the User$ store with hydrated data */function UserProfile() { const { $user } = useInject(User$) const user = useSignal($user)
/* ... */}StaticHydrator is a one-shot hydrator: it applies values from the initial dehydrated snapshot and discards them after use. For streaming SSR where chunks of dehydrated data arrive after the initial render, use ActiveHydrator instead — it exposes a push(dehydrated) method to feed additional data reactively.