From 14c3e08ad3a7c0875bae6e236b9f79951fa08dd3 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Mon, 9 Mar 2026 08:27:07 -0500 Subject: [PATCH] refactor usePolygonStates to use ws topic subscription --- web/src/hooks/use-polygon-states.ts | 68 +++++++++++++++++++++++++---- 1 file changed, 60 insertions(+), 8 deletions(-) diff --git a/web/src/hooks/use-polygon-states.ts b/web/src/hooks/use-polygon-states.ts index 6eea109db..7cb55faf0 100644 --- a/web/src/hooks/use-polygon-states.ts +++ b/web/src/hooks/use-polygon-states.ts @@ -1,18 +1,70 @@ -import { useMemo } from "react"; +import { useCallback, useMemo, useSyncExternalStore } from "react"; import { Polygon } from "@/types/canvas"; -import { useWsState } from "@/api/ws"; +import { subscribeWsTopic, getWsTopicValue } from "@/api/ws"; /** * Hook to get enabled state for a polygon from websocket state. - * Memoizes the lookup function to avoid unnecessary re-renders. + * Subscribes to all relevant per-polygon topics so it only re-renders + * when one of those specific topics changes — not on every WS update. */ export function usePolygonStates(polygons: Polygon[]) { - const wsState = useWsState(); + // Build a stable sorted list of topics we need to watch + const topics = useMemo(() => { + const set = new Set(); + polygons.forEach((polygon) => { + const topic = + polygon.type === "zone" + ? `${polygon.camera}/zone/${polygon.name}/state` + : polygon.type === "motion_mask" + ? `${polygon.camera}/motion_mask/${polygon.name}/state` + : `${polygon.camera}/object_mask/${polygon.name}/state`; + set.add(topic); + }); + return Array.from(set).sort(); + }, [polygons]); - // Create a memoized lookup map that only updates when relevant ws values change + // Stable key for the topic list so subscribe/getSnapshot stay in sync + const topicsKey = topics.join("\0"); + + // Subscribe to all topics at once — re-subscribe only when the set changes + const subscribe = useCallback( + (listener: () => void) => { + const unsubscribes = topicsKey + .split("\0") + .filter(Boolean) + .map((topic) => subscribeWsTopic(topic, listener)); + return () => unsubscribes.forEach((unsub) => unsub()); + }, + [topicsKey], + ); + + // Build a snapshot string from the current values of all topics. + // useSyncExternalStore uses Object.is, so we return a primitive that + // changes only when an observed topic's value changes. + const getSnapshot = useCallback(() => { + return topicsKey + .split("\0") + .filter(Boolean) + .map((topic) => `${topic}=${getWsTopicValue(topic) ?? ""}`) + .join("\0"); + }, [topicsKey]); + + const snapshot = useSyncExternalStore(subscribe, getSnapshot); + + // Parse the snapshot into a lookup map return useMemo(() => { - const stateMap = new Map(); + // Build value map from snapshot + const valueMap = new Map(); + snapshot.split("\0").forEach((entry) => { + const eqIdx = entry.indexOf("="); + if (eqIdx > 0) { + const topic = entry.slice(0, eqIdx); + const val = entry.slice(eqIdx + 1) || undefined; + valueMap.set(topic, val); + } + }); + const stateMap = new Map(); polygons.forEach((polygon) => { const topic = polygon.type === "zone" @@ -21,7 +73,7 @@ export function usePolygonStates(polygons: Polygon[]) { ? `${polygon.camera}/motion_mask/${polygon.name}/state` : `${polygon.camera}/object_mask/${polygon.name}/state`; - const wsValue = wsState[topic]; + const wsValue = valueMap.get(topic); const enabled = wsValue === "ON" ? true @@ -40,5 +92,5 @@ export function usePolygonStates(polygons: Polygon[]) { true ); }; - }, [polygons, wsState]); + }, [polygons, snapshot]); }