multica/apps/mobile/lib/use-ws-subscriptions.ts

52 lines
1.8 KiB
TypeScript

/**
* Boilerplate-killer for the Layer-3 realtime hooks.
*
* Every `use-X-realtime` hook used to repeat this same shape:
*
* const ws = useWSClient();
* const wsId = useWorkspaceStore(s => s.currentWorkspaceId);
* useEffect(() => {
* if (!ws || !wsId) return;
* const unsubs = [ ws.on(...), ws.on(...), ws.onReconnect(...) ];
* return () => { for (const u of unsubs) u(); };
* }, [ws, wsId, ...]);
*
* `useWSSubscriptions(setup, deps)` collapses the lifecycle scaffolding so
* each hook focuses on its actual subscription list. The `setup` callback
* runs only when both ws + wsId are non-null, receives them as args, and
* returns the array of unsubs to wire into cleanup.
*
* Per-record hooks pass their record id via the deps array (so it
* re-subscribes when navigating between records). They can also access
* other narrow-scoped values via closure from the calling component.
*/
import { useEffect } from "react";
import type { WSClient } from "@/data/realtime/ws-client";
import { useWSClient } from "@/data/realtime/realtime-provider";
import { useWorkspaceStore } from "@/data/workspace-store";
export type WSSubscriptionSetup = (
ws: WSClient,
wsId: string,
) => (() => void)[] | undefined;
export function useWSSubscriptions(
setup: WSSubscriptionSetup,
deps: readonly unknown[],
) {
const ws = useWSClient();
const wsId = useWorkspaceStore((s) => s.currentWorkspaceId);
useEffect(() => {
if (!ws || !wsId) return;
const unsubs = setup(ws, wsId) ?? [];
return () => {
for (const u of unsubs) u();
};
// setup is intentionally NOT in deps — callers control re-subscription
// via the explicit `deps` array. Putting setup in deps would re-fire
// on every render (closures), defeating the whole point.
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ws, wsId, ...deps]);
}