rwsdk
Version:
Build fast, server-driven webapps on Cloudflare with SSR, RSC, and realtime
65 lines (64 loc) • 2.78 kB
JavaScript
import React from "react";
import { getSyncedStateClient } from "./client-core.js";
import { DEFAULT_SYNCED_STATE_PATH } from "./constants.mjs";
const defaultDeps = {
useState: React.useState,
useEffect: React.useEffect,
useRef: React.useRef,
useCallback: React.useCallback,
};
/**
* Builds a `useSyncedState` hook configured with optional endpoint and hook overrides.
* @param options Optional overrides for endpoint and React primitives.
* @returns Hook that syncs state through the sync state service.
*/
export const createSyncedStateHook = (options = {}) => {
const basePath = options.url ?? DEFAULT_SYNCED_STATE_PATH;
const deps = options.hooks ?? defaultDeps;
const { useState, useEffect, useRef, useCallback } = deps;
return function useSyncedState(initialValue, key, roomId = options.roomId) {
if (typeof window === "undefined" && !options.hooks) {
return [initialValue, () => { }];
}
const resolvedUrl = roomId ? `${basePath}/${roomId}` : basePath;
const client = getSyncedStateClient(resolvedUrl);
const [value, setValue] = useState(initialValue);
const valueRef = useRef(value);
valueRef.current = value;
const setSyncValue = useCallback((nextValue) => {
const resolved = typeof nextValue === "function"
? nextValue(valueRef.current)
: nextValue;
setValue(resolved);
valueRef.current = resolved;
void client.setState(resolved, key);
}, [client, key, setValue, valueRef]);
useEffect(() => {
let isActive = true;
const handleUpdate = (next) => {
if (isActive) {
setValue(next);
valueRef.current = next;
}
};
void client.getState(key).then((existing) => {
if (existing !== undefined && isActive) {
setValue(existing);
valueRef.current = existing;
}
});
void client.subscribe(key, handleUpdate);
return () => {
isActive = false;
// Call unsubscribe when component unmounts
// Page reloads are handled by the beforeunload event listener in client-core.ts
void client.unsubscribe(key, handleUpdate).catch((error) => {
// Log but don't throw - cleanup should not prevent unmounting
console.error("[useSyncedState] Error during unsubscribe:", error);
});
};
}, [client, key, setValue, valueRef]);
return [value, setSyncValue];
};
};
export const useSyncedState = createSyncedStateHook();