Cross-client state synchronization using Durable Objects with React hooks for seamless state updates across connected clients.
React hook for synchronized state across clients, similar to useState but synced via Durable Objects.
/**
* React hook for synced state across clients
* @param initialValue - Initial state value
* @param key - Unique key for this state (used for synchronization)
* @returns Tuple of [state, setState] like useState
*/
function useSyncedState<T>(
initialValue: T,
key: string
): [T, (value: T | ((prev: T) => T)) => void];Usage Example:
import { useSyncedState } from 'rwsdk/use-synced-state/client';
function Counter() {
const [count, setCount] = useSyncedState(0, 'global-counter');
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>Increment</button>
<button onClick={() => setCount((prev) => prev - 1)}>Decrement</button>
</div>
);
}
// All clients viewing this component will see the same count
// Changes from any client are immediately reflected in all othersCreates a custom synced state hook with specific configuration.
/**
* Creates a custom synced state hook
* @param options - Configuration options
* @returns Custom useSyncedState hook
*/
function createSyncedStateHook(options?: {
/** Custom endpoint URL for synced state server */
url?: string;
/** Custom React hooks (for testing) */
hooks?: HookDeps;
}): typeof useSyncedState;
interface HookDeps {
useState: typeof import('react').useState;
useEffect: typeof import('react').useEffect;
}Low-level client interface for synced state operations.
/**
* Gets or creates a synced state client instance
* @param endpoint - Optional custom endpoint
* @returns Synced state client
*/
function getSyncedStateClient(endpoint?: string): SyncedStateClient;
/**
* Initializes a new synced state client
* @param options - Initialization options
* @returns Synced state client
*/
function initSyncedStateClient(options?: {
endpoint?: string;
}): SyncedStateClient;
/**
* Sets client instance for testing
* @param client - Mock client
* @param endpoint - Endpoint key
*/
function setSyncedStateClientForTesting(
client: SyncedStateClient,
endpoint?: string
): void;
interface SyncedStateClient {
/**
* Gets state value by key
* @param key - State key
* @returns Current state value
*/
getState(key: string): Promise<unknown>;
/**
* Sets state value by key
* @param value - New state value
* @param key - State key
*/
setState(value: unknown, key: string): Promise<void>;
/**
* Subscribes to state changes
* @param key - State key to watch
* @param handler - Callback for state changes
*/
subscribe(key: string, handler: (value: unknown) => void): Promise<void>;
/**
* Unsubscribes from state changes
* @param key - State key
* @param handler - Handler to remove
*/
unsubscribe(key: string, handler: (value: unknown) => void): Promise<void>;
}Durable Object for managing synced state on the server.
/**
* Durable Object for synced state management
*/
class SyncedStateServer {
/**
* Registers a key transformation handler (static)
* @param handler - Function to transform keys
*/
static registerKeyHandler(
handler: (key: string) => Promise<string>
): void;
/**
* Gets the registered key handler (static)
* @returns Current key handler or null
*/
static getKeyHandler(): ((key: string) => Promise<string>) | null;
/**
* Registers a setState interceptor (static)
* @param handler - Handler called before setState
*/
static registerSetStateHandler(handler: OnSetHandler | null): void;
/**
* Registers a getState interceptor (static)
* @param handler - Handler called before getState
*/
static registerGetStateHandler(handler: OnGetHandler | null): void;
/**
* Gets state by key
* @param key - State key
* @returns Current state value
*/
getState(key: string): SyncedStateValue;
/**
* Sets state by key and notifies subscribers
* @param value - New state value
* @param key - State key
*/
setState(value: SyncedStateValue, key: string): void;
/**
* Subscribes a client to state changes
* @param key - State key
* @param client - RPC stub for the client
*/
subscribe(key: string, client: RpcStub): void;
/**
* Unsubscribes a client from state changes
* @param key - State key
* @param client - RPC stub for the client
*/
unsubscribe(key: string, client: RpcStub): void;
/**
* Handles HTTP requests
* @param request - Incoming request
* @returns Response
*/
fetch(request: Request): Promise<Response>;
}
type SyncedStateValue = unknown;
type OnSetHandler = (key: string, value: unknown) => Promise<void>;
type OnGetHandler = (key: string) => Promise<unknown>;Creates route definitions for synced state endpoints.
/**
* Creates route definitions for synced state
* @param getNamespace - Function to get Durable Object namespace
* @param options - Route configuration
* @returns Array of route definitions
*/
function syncedStateRoutes(
getNamespace: (env: any) => DurableObjectNamespace<SyncedStateServer>,
options?: SyncedStateRouteOptions
): Route<any>[];
interface SyncedStateRouteOptions {
/** Base path for routes (default: "/synced-state") */
basePath?: string;
/** Durable Object name (default: "default") */
durableObjectName?: string;
}// worker.tsx
import { defineApp, route, render } from 'rwsdk/worker';
import { syncedStateRoutes, SyncedStateServer } from 'rwsdk/use-synced-state/worker';
function Document({ children }) {
return (
<html>
<head>
<title>Synced State App</title>
</head>
<body>{children}</body>
</html>
);
}
function Counter() {
'use client';
const [count, setCount] = useSyncedState(0, 'app-counter');
return (
<div>
<p>Shared Count: {count}</p>
<button onClick={() => setCount(count + 1)}>+1</button>
</div>
);
}
export default defineApp([
// Add synced state routes
...syncedStateRoutes((env) => env.SYNCED_STATE_DO),
// Regular routes
render(Document, [
route('/', Counter),
]),
]);
// Export Durable Object
export { SyncedStateServer };// client.tsx
import { initClient } from 'rwsdk/client';
initClient();
// useSyncedState is automatically configured
// No additional client-side setup needed# wrangler.toml
name = "synced-state-app"
[[durable_objects.bindings]]
name = "SYNCED_STATE_DO"
class_name = "SyncedStateServer"
script_name = "synced-state-app"import { SyncedStateServer } from 'rwsdk/use-synced-state/worker';
// Transform keys to include user-specific prefixes
SyncedStateServer.registerKeyHandler(async (key) => {
const userId = await getCurrentUserId();
return `${userId}:${key}`;
});import { SyncedStateServer } from 'rwsdk/use-synced-state/worker';
// Persist state changes to KV
SyncedStateServer.registerSetStateHandler(async (key, value) => {
await env.KV.put(`state:${key}`, JSON.stringify(value));
});
// Load state from KV
SyncedStateServer.registerGetStateHandler(async (key) => {
const stored = await env.KV.get(`state:${key}`);
return stored ? JSON.parse(stored) : undefined;
});// Different base paths for different state groups
const userStateRoutes = syncedStateRoutes(
(env) => env.USER_STATE_DO,
{ basePath: '/user-state' }
);
const appStateRoutes = syncedStateRoutes(
(env) => env.APP_STATE_DO,
{ basePath: '/app-state' }
);
export default defineApp([
...userStateRoutes,
...appStateRoutes,
// ... other routes
]);import { useSyncedState } from 'rwsdk/use-synced-state/client';
interface TodoList {
items: Array<{ id: string; text: string; done: boolean }>;
}
function TodoApp() {
const [todos, setTodos] = useSyncedState<TodoList>(
{ items: [] },
'todo-list'
);
const addTodo = (text: string) => {
setTodos((prev) => ({
items: [
...prev.items,
{ id: crypto.randomUUID(), text, done: false },
],
}));
};
const toggleTodo = (id: string) => {
setTodos((prev) => ({
items: prev.items.map((item) =>
item.id === id ? { ...item, done: !item.done } : item
),
}));
};
return (
<div>
<ul>
{todos.items.map((item) => (
<li key={item.id} onClick={() => toggleTodo(item.id)}>
{item.done ? '✓' : '○'} {item.text}
</li>
))}
</ul>
<button onClick={() => addTodo('New task')}>Add Todo</button>
</div>
);
}