mirror of
https://github.com/blakeblackshear/frigate.git
synced 2026-04-22 23:17:17 +08:00
192aba901a
* refactor websockets to remove react-tracked
react 19 removed useReducer eager bailout, which broke react-tracked.
react-tracked works by wrapping state in a JavaScript Proxy. When a component reads state.someField, the proxy records that access. On the next state update, it compares only the fields each component actually touched and skips re-renders if those fields are unchanged. Under the hood, this relies on useReducer — and in React 18, useReducer had an "eager bail-out" that short-circuited rendering when the new state was === to the old state. React 19 removed that optimization, so every dispatch now schedules a render regardless, and the proxy comparison runs too late to prevent it.
useSyncExternalStore is a React primitive (added in 18, stable in 19) designed for exactly this pattern: subscribing to an external store:
useSyncExternalStore(
subscribe, // (listener) => unsubscribe — called when the store changes
getSnapshot // () => value — returns the current value for this subscriber
)
React calls getSnapshot during render and compares the result with Object.is. If the value is the same reference, the component bails out — no re-render. The key difference from react-tracked is that this bail-out is built into React's reconciler, not bolted on via proxy tricks and useReducer.
The per-topic subscription model makes this efficient. Instead of one global store where every subscriber has to check if their fields changed, each useWs("some/topic", ...) call subscribes only to that topic's listener set. When a message arrives for front_door/detect/state, only components subscribed to that exact topic get their listener fired → React calls their getSnapshot → Object.is compares the value → bail-out if unchanged. Components watching back_yard/detect/state are never even notified.
* remove react-tracked and react-use-websocket
* refactor usePolygonStates to use ws topic subscription
* fix TimeAgo refresh interval always returning 1s due to unit mismatch (seconds vs milliseconds)
older events now correctly refresh every minute/hour instead of every second
* simplify
* clean up
* don't resend onconnect
* clean up
* remove patch
79 lines
2.0 KiB
TypeScript
79 lines
2.0 KiB
TypeScript
import { baseUrl } from "./baseUrl";
|
|
import { ReactNode, useCallback, useEffect, useRef } from "react";
|
|
import { WsSendContext } from "./wsContext";
|
|
import type { Update } from "./wsContext";
|
|
import { processWsMessage, resetWsStore } from "./ws";
|
|
|
|
export function WsProvider({ children }: { children: ReactNode }) {
|
|
const wsUrl = `${baseUrl.replace(/^http/, "ws")}ws`;
|
|
const wsRef = useRef<WebSocket | null>(null);
|
|
const reconnectTimer = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
const reconnectAttempt = useRef(0);
|
|
const unmounted = useRef(false);
|
|
|
|
const sendJsonMessage = useCallback((msg: unknown) => {
|
|
if (wsRef.current?.readyState === WebSocket.OPEN) {
|
|
wsRef.current.send(JSON.stringify(msg));
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
unmounted.current = false;
|
|
|
|
function connect() {
|
|
if (unmounted.current) return;
|
|
|
|
const ws = new WebSocket(wsUrl);
|
|
wsRef.current = ws;
|
|
|
|
ws.onopen = () => {
|
|
reconnectAttempt.current = 0;
|
|
ws.send(
|
|
JSON.stringify({ topic: "onConnect", message: "", retain: false }),
|
|
);
|
|
};
|
|
|
|
ws.onmessage = (event: MessageEvent) => {
|
|
processWsMessage(event.data as string);
|
|
};
|
|
|
|
ws.onclose = () => {
|
|
if (unmounted.current) return;
|
|
const delay = Math.min(1000 * 2 ** reconnectAttempt.current, 30000);
|
|
reconnectAttempt.current++;
|
|
reconnectTimer.current = setTimeout(connect, delay);
|
|
};
|
|
|
|
ws.onerror = () => {
|
|
ws.close();
|
|
};
|
|
}
|
|
|
|
connect();
|
|
|
|
return () => {
|
|
unmounted.current = true;
|
|
if (reconnectTimer.current) {
|
|
clearTimeout(reconnectTimer.current);
|
|
}
|
|
wsRef.current?.close();
|
|
resetWsStore();
|
|
};
|
|
}, [wsUrl]);
|
|
|
|
const send = useCallback(
|
|
(message: Update) => {
|
|
sendJsonMessage({
|
|
topic: message.topic,
|
|
payload: message.payload,
|
|
retain: message.retain,
|
|
});
|
|
},
|
|
[sendJsonMessage],
|
|
);
|
|
|
|
return (
|
|
<WsSendContext.Provider value={send}>{children}</WsSendContext.Provider>
|
|
);
|
|
}
|