Skip to content

useSyncedState Experimental

useSyncedState keeps a value aligned across tabs, devices, or users by using the realtime transport and the SyncedStateServer Durable Object.


src/hooks/useSyncedState.ts
import { useSyncedState } from "rwsdk/use-sync-state";
src/hooks/useSyncedState.ts
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.

src/worker.tsx
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...
]);

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.

src/worker.tsx
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.


src/components/Counter.tsx
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.


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.

src/worker.tsx
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:

src/components/UserCounter.tsx
"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.


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.
src/hooks/useCustomSyncedState.ts
const useCustomSyncedState = createSyncedStateHook({ url: "/api/sync" });
src/hooks/useSyncedState.test.ts
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:

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>;
}

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(),
});

Failed operations will automatically retry with exponential backoff, handling transient network failures without user intervention.