useSyncedState Experimental
useSyncedState keeps a value aligned across tabs, devices, or users by using the realtime transport and the SyncedStateServer Durable Object.
1. Default Hook
Section titled “1. Default Hook”import { useSyncedState } from "rwsdk/use-sync-state";2. Hook Factory (optional)
Section titled “2. Hook Factory (optional)”import { useCallback, useEffect, useRef, useState } from "react";import { createSyncedStateHook } from "rwsdk/use-sync-state";
export const useSyncedState = createSyncedStateHook({ url: "/sync-state", hooks: { useState, useEffect, useRef, useCallback, },});Set the URL to match the base path you expose from the worker routes. In this example the worker would forward requests from /sync-state instead of the default /__sync-state.
3. Worker Routes
Section titled “3. Worker Routes”import { SyncedStateServer, SyncedStateRoutes,} from "rwsdk/use-sync-state/worker";import { env } from "cloudflare:workers";
SyncedStateServer.registerGetStateHandler((key, value) => { console.log("synced state requested", { key, value });});SyncedStateServer.registerSetStateHandler((key, value) => { console.log("synced state updated", { key, value });});
export default defineApp([ ...SyncedStateRoutes(() => env.STATE_COORDINATOR), // other routes...]);4. State handlers
Section titled “4. State handlers”SyncedStateServer.registerSetStateHandler(handler) accepts a function that receives (key, value) each time the coordinator stores a new value. Use this to record updates or trigger downstream work whenever any client publishes new state.
SyncedStateServer.registerGetStateHandler(handler) accepts a function that receives (key, value) each time the coordinator returns a value to a subscriber. The handler runs after the lookup, so value is undefined when no entry exists for the key.
5. Durable Object Export
Section titled “5. Durable Object Export”export { SyncedStateServer } from "rwsdk/use-sync-state";6. Add the Durable Object to wrangler.jsonc
Section titled “6. Add the Durable Object to wrangler.jsonc”"durable_objects": { "bindings": [ // ... { "name": "STATE_COORDINATOR", "class_name": "SyncedStateServer", }, ],},After updating wrangler.jsonc, run pnpm generate to update the generated type definitions.
const Counter = () => { const [count, setCount] = useSyncedState(0, "counter"); return <button onClick={() => setCount((n) => n + 1)}>{count}</button>;};setCount applies the update locally and forwards it to the coordinator, which broadcasts the new value to every subscriber using the realtime transport.
Per-User State Scoping
Section titled “Per-User State Scoping”To scope synced state to individual users, register a key transformation handler in your worker. The handler runs on every state operation and can transform client-provided keys based on the authenticated session.
import { SyncedStateServer } from "rwsdk/use-sync-state/worker";import { sessionStore } from "./session";import { requestInfo } from "rwsdk/worker";
SyncedStateServer.registerKeyHandler(async (key) => { const session = await sessionStore.load(requestInfo.request); const userId = session?.userId ?? "anonymous"; return `user:${userId}:${key}`;});With this handler registered, client components use simple, unscoped keys:
"use client";
import { useSyncedState } from "rwsdk/use-sync-state";
export const UserCounter = () => { const [count, setCount] = useSyncedState(0, "counter"); return <button onClick={() => setCount((n) => n + 1)}>{count}</button>;};The worker transforms "counter" to "user:123:counter" before storing or retrieving state. Each user sees their own counter value without needing to pass user IDs from the client.
If the handler throws an error, that error propagates to the client where it can be caught using error boundaries or try-catch blocks in event handlers.
Customizing createSyncedStateHook
Section titled “Customizing createSyncedStateHook”createSyncedStateHook({ url?, hooks? }) accepts an options object:
url: overrides the HTTP route used by the client. The default is/__sync-state.hooks: overrides the React primitives. Pass this when integrating with a custom renderer or testing environment.
const useCustomSyncedState = createSyncedStateHook({ url: "/api/sync" });const useTestSyncedState = createSyncedStateHook({ hooks: { useState, useEffect, useRef, useCallback, },});Future: Error Handling and Offline Support
Section titled “Future: Error Handling and Offline Support”Future versions will include:
Connection Status Monitoring
Section titled “Connection Status Monitoring”A separate useSyncedStateStatus hook will expose connection state and sync progress:
const [count, setCount] = useSyncedState(0, "counter");const { connected, syncing, error } = useSyncedStateStatus("counter");
if (!connected) { return <Banner>Working offline - changes will sync when reconnected</Banner>;}Offline Queue with Pluggable Storage
Section titled “Offline Queue with Pluggable Storage”An offline queue will store failed operations and retry them when connectivity returns. The queue interface will be pluggable, allowing you to choose between:
- InMemoryQueue (default): Fast, no setup, lost on page refresh
- IndexedDBQueue: Persists across page reloads
- SqliteQueue: Browser SQLite for complex offline scenarios
- Custom implementations: Use any storage backend
import { initSyncedStateClient } from "rwsdk/use-sync-state";import { IndexedDBQueue } from "rwsdk/use-sync-state/queues";
initSyncedStateClient({ queue: new IndexedDBQueue(),});Automatic Retry Logic
Section titled “Automatic Retry Logic”Failed operations will automatically retry with exponential backoff, handling transient network failures without user intervention.