From 89864e364dbc891bfae8bae3b326e8259a7afc3f Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 16 Mar 2026 09:54:16 +0000 Subject: [PATCH] fix: prevent infinite render loop in zoom overlay callbacks MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use useRef to store onStatsUpdate/onLoadingChange/onActiveMotionChange callbacks so useEffect deps don't include the callback references. Inline arrow functions in .map() change identity every render, causing the previous useEffect([stats, onCallback]) to re-fire on each parent re-render, triggering another setState → re-render → infinite loop. https://claude.ai/code/session_019B4dJXtcxvHn97ZaqHUB62 --- web/src/components/player/LivePlayer.tsx | 41 +++++++++--------------- 1 file changed, 16 insertions(+), 25 deletions(-) diff --git a/web/src/components/player/LivePlayer.tsx b/web/src/components/player/LivePlayer.tsx index af0511ac1..e0959e555 100644 --- a/web/src/components/player/LivePlayer.tsx +++ b/web/src/components/player/LivePlayer.tsx @@ -111,9 +111,16 @@ export default function LivePlayer({ droppedFrameRate: 0, // percentage }); + const onStatsUpdateRef = useRef(onStatsUpdate); + const onLoadingChangeRef = useRef(onLoadingChange); + const onActiveMotionChangeRef = useRef(onActiveMotionChange); + onStatsUpdateRef.current = onStatsUpdate; + onLoadingChangeRef.current = onLoadingChange; + onActiveMotionChangeRef.current = onActiveMotionChange; + useEffect(() => { - onStatsUpdate?.(stats); - }, [stats, onStatsUpdate]); + onStatsUpdateRef.current?.(stats); + }, [stats]); // camera activity @@ -285,40 +292,24 @@ export default function LivePlayer({ }, [liveReady, isReEnabling]); useEffect(() => { - if (!onLoadingChange) return; const loading = !!( cameraEnabled && !offline && (!showStillWithoutActivity || isReEnabling) && !liveReady ); - onLoadingChange(loading); - }, [ - onLoadingChange, - cameraEnabled, - offline, - showStillWithoutActivity, - isReEnabling, - liveReady, - ]); + onLoadingChangeRef.current?.(loading); + }, [cameraEnabled, offline, showStillWithoutActivity, isReEnabling, liveReady]); useEffect(() => { - if (!onActiveMotionChange) return; const motionVisible = !!( autoLive && !offline && activeMotion && ((showStillWithoutActivity && !liveReady) || liveReady) ); - onActiveMotionChange(motionVisible); - }, [ - onActiveMotionChange, - autoLive, - offline, - activeMotion, - showStillWithoutActivity, - liveReady, - ]); + onActiveMotionChangeRef.current?.(motionVisible); + }, [autoLive, offline, activeMotion, showStillWithoutActivity, liveReady]); if (!cameraConfig) { return ; @@ -453,7 +444,7 @@ export default function LivePlayer({ /> - {!onLoadingChange && + {!onLoadingChangeRef.current && cameraEnabled && !offline && (!showStillWithoutActivity || isReEnabling) && @@ -577,7 +568,7 @@ export default function LivePlayer({ {cameraName} )} - {!onActiveMotionChange && + {!onActiveMotionChangeRef.current && autoLive && !offline && activeMotion && @@ -585,7 +576,7 @@ export default function LivePlayer({ )} - {showStats && !onStatsUpdate && ( + {showStats && !onStatsUpdateRef.current && ( )}