The Hydration module provides utilities for serializing and deserializing atom state, enabling server-side rendering (SSR) and state persistence. It allows you to capture atom state on the server and hydrate it on the client for seamless SSR experiences.
Serializes the current state of all serializable atoms in a registry. This is typically called on the server after rendering to capture the current state.
import * as Registry from "@effect-atom/atom/Registry"import * as Hydration from "@effect-atom/atom/Hydration"// On the server, after renderingconst registry = Registry.make()// ... render your app and populate the registry ...// Dehydrate the stateconst dehydratedState = Hydration.dehydrate(registry, { encodeInitialAs: "promise" // Include promises for pending atoms})// Serialize and send to clientconst serialized = JSON.stringify(dehydratedState)
Only atoms marked as serializable using Atom.makeSerializable or Atom.withSerializable will be included in the dehydrated state.
const dehydratedState = Hydration.dehydrate(registry)const values = Hydration.toValues(dehydratedState)// Now you can access the value propertiesvalues.forEach(atom => { console.log(atom.key, atom.value, atom.dehydratedAt)})
import * as Registry from "@effect-atom/atom/Registry"import * as Hydration from "@effect-atom/atom/Hydration"// On the clientconst registry = Registry.make()// Get the dehydrated state from the server (e.g., from a script tag)const dehydratedState = JSON.parse( document.getElementById('__ATOM_STATE__')?.textContent || '[]')// Hydrate the registryHydration.hydrate(registry, dehydratedState)// Now the registry contains the server-rendered state
The dehydrated state must contain atoms that are defined in your application. If an atom key is not recognized, the value will be ignored.
import { hydrateRoot } from 'react-dom/client'import * as Registry from "@effect-atom/atom/Registry"import * as Hydration from "@effect-atom/atom/Hydration"import { RegistryProvider } from "@effect-atom/atom-react"import App from './App'// Create client-side registryconst registry = Registry.make()// Get dehydrated state from the pageconst dehydratedStateElement = document.getElementById('__ATOM_STATE__')if (dehydratedStateElement) { const dehydratedState = JSON.parse(dehydratedStateElement.textContent || '[]') // Hydrate the registry Hydration.hydrate(registry, dehydratedState)}// Hydrate the React apphydrateRoot( document.getElementById('root'), <RegistryProvider registry={registry}> <App /> </RegistryProvider>)
import { useAtomValue } from "@effect-atom/atom-react"import { MyApiClient } from './api'function UserProfile({ userId }: { userId: string }) { // This atom will be hydrated with server data const userAtom = MyApiClient.query("Users", "getUser", { path: { id: userId } }) const user = useAtomValue(userAtom) return Result.match(user, { // On first render, this will show immediately with server data Success: (data) => ( <div> <h1>{data.name}</h1> <p>{data.email}</p> </div> ), Failure: (error) => <div>Error: {error.message}</div>, Initial: () => <div>Loading...</div>, Pending: () => <div>Loading...</div> })}
When using encodeInitialAs: "promise", atoms that are still loading during server rendering will include a promise in the dehydrated state. This enables streaming SSR:
// Server-sideconst dehydratedState = Hydration.dehydrate(registry, { encodeInitialAs: "promise"})const values = Hydration.toValues(dehydratedState)// Send initial HTML immediatelyres.write(initialHtml)// Stream updates as atoms resolvefor (const atom of values) { if (atom.resultPromise) { const resolvedValue = await atom.resultPromise res.write(` <script> window.__updateAtom('${atom.key}', ${JSON.stringify(resolvedValue)}) </script> `) }}res.end()
Streaming SSR requires additional client-side code to handle progressive hydration updates. The above example shows the general pattern.
Only atoms explicitly marked as serializable will be included in dehydration:
import * as Atom from "@effect-atom/atom/Atom"// Make an atom serializableconst myAtom = Atom.makeSerializable( Atom.make(Effect.succeed("initial value")), { encode: (value) => value, decode: (value) => value })
If client-side atom definitions don’t match server-side definitions, hydration will silently skip those atoms:
// Ensure atom definitions are shared between server and client// Bad: Defining atoms in component files// Good: Define atoms in shared modules imported by both
Be careful with sensitive data. Dehydrated state is sent to the client and visible in the page source. Don’t include secrets or private user data in dehydrated atoms.