Frontend fixes (#22294)

* fix useImageLoaded hook running on every render

* fix volume not applying for all cameras

* Fix maximum update depth exceeded errors on Review page

- use-overlay-state: use refs for location to keep setter identity
  stable across renders, preventing cascading re-render loops when
  effects depend on the setter. Add Object.is bail-out guard to skip
  redundant navigate calls. Move setPersistedValue after bail-out to
  avoid unnecessary IndexedDB writes.

* don't try to fetch previews when motion search dialog is open

* revert unneeded changes

re-rendering was caused by the overlay state hook, not this one

* filter dicts to only use id field in sync recordings
This commit is contained in:
Josh Hawkins 2026-03-06 14:41:15 -06:00 committed by GitHub
parent 34cc1208a6
commit c9bd907721
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 60 additions and 29 deletions

View File

@ -151,7 +151,9 @@ def sync_recordings(
max_inserts = 1000
for batch in chunked(recordings_to_delete, max_inserts):
RecordingsToDelete.insert_many(batch).execute()
RecordingsToDelete.insert_many(
[{"id": r["id"]} for r in batch]
).execute()
try:
deleted = (

View File

@ -1,4 +1,4 @@
import { useCallback, useContext, useEffect, useMemo } from "react";
import { useCallback, useContext, useEffect, useMemo, useRef } from "react";
import { useLocation, useNavigate, useSearchParams } from "react-router-dom";
import { usePersistence } from "./use-persistence";
import { useUserPersistence } from "./use-user-persistence";
@ -12,20 +12,28 @@ export function useOverlayState<S>(
const location = useLocation();
const navigate = useNavigate();
const currentLocationState = useMemo(() => location.state, [location]);
const locationRef = useRef(location);
locationRef.current = location;
const setOverlayStateValue = useCallback(
(value: S, replace: boolean = false) => {
const newLocationState = { ...currentLocationState };
const loc = locationRef.current;
const currentValue = loc.state?.[key] as S | undefined;
if (Object.is(currentValue, value)) {
return;
}
const newLocationState = { ...loc.state };
newLocationState[key] = value;
navigate(location.pathname + (preserveSearch ? location.search : ""), {
navigate(loc.pathname + (preserveSearch ? loc.search : ""), {
state: newLocationState,
replace,
});
},
// we know that these deps are correct
// locationRef is stable so we don't need it in deps
// eslint-disable-next-line react-hooks/exhaustive-deps
[key, currentLocationState, navigate],
[key, navigate, preserveSearch],
);
const overlayStateValue = useMemo<S | undefined>(
@ -47,7 +55,9 @@ export function usePersistedOverlayState<S extends string>(
] {
const location = useLocation();
const navigate = useNavigate();
const currentLocationState = useMemo(() => location.state, [location]);
const locationRef = useRef(location);
locationRef.current = location;
// currently selected value
@ -63,14 +73,21 @@ export function usePersistedOverlayState<S extends string>(
const setOverlayStateValue = useCallback(
(value: S | undefined, replace: boolean = false) => {
const loc = locationRef.current;
const currentValue = loc.state?.[key] as S | undefined;
if (Object.is(currentValue, value)) {
return;
}
setPersistedValue(value);
const newLocationState = { ...currentLocationState };
const newLocationState = { ...loc.state };
newLocationState[key] = value;
navigate(location.pathname, { state: newLocationState, replace });
navigate(loc.pathname, { state: newLocationState, replace });
},
// we know that these deps are correct
// locationRef is stable so we don't need it in deps
// eslint-disable-next-line react-hooks/exhaustive-deps
[key, currentLocationState, navigate],
[key, navigate, setPersistedValue],
);
return [
@ -98,7 +115,9 @@ export function useUserPersistedOverlayState<S extends string>(
const { auth } = useContext(AuthContext);
const location = useLocation();
const navigate = useNavigate();
const currentLocationState = useMemo(() => location.state, [location]);
const locationRef = useRef(location);
locationRef.current = location;
// currently selected value from URL state
const overlayStateValue = useMemo<S | undefined>(
@ -112,14 +131,21 @@ export function useUserPersistedOverlayState<S extends string>(
const setOverlayStateValue = useCallback(
(value: S | undefined, replace: boolean = false) => {
const loc = locationRef.current;
const currentValue = loc.state?.[key] as S | undefined;
if (Object.is(currentValue, value)) {
return;
}
setPersistedValue(value);
const newLocationState = { ...currentLocationState };
const newLocationState = { ...loc.state };
newLocationState[key] = value;
navigate(location.pathname, { state: newLocationState, replace });
navigate(loc.pathname, { state: newLocationState, replace });
},
// we know that these deps are correct
// locationRef is stable so we don't need it in deps
// eslint-disable-next-line react-hooks/exhaustive-deps
[key, currentLocationState, navigate, setPersistedValue],
[key, navigate, setPersistedValue],
);
// Don't return a value until auth has finished loading
@ -142,17 +168,21 @@ export function useHashState<S extends string>(): [
const location = useLocation();
const navigate = useNavigate();
const locationRef = useRef(location);
locationRef.current = location;
const setHash = useCallback(
(value: S | undefined) => {
const loc = locationRef.current;
if (!value) {
navigate(location.pathname);
navigate(loc.pathname);
} else {
navigate(`${location.pathname}#${value}`, { state: location.state });
navigate(`${loc.pathname}#${value}`, { state: loc.state });
}
},
// we know that these deps are correct
// locationRef is stable so we don't need it in deps
// eslint-disable-next-line react-hooks/exhaustive-deps
[location, navigate],
[navigate],
);
const hash = useMemo(

View File

@ -632,9 +632,10 @@ export default function DraggableGridLayout({
toggleStats={() => toggleStats(camera.name)}
volumeState={volumeStates[camera.name]}
setVolumeState={(value) =>
setVolumeStates({
setVolumeStates((prev) => ({
...prev,
[camera.name]: value,
})
}))
}
muteAll={muteAll}
unmuteAll={unmuteAll}

View File

@ -131,12 +131,10 @@ export default function MotionSearchView({
);
// Camera previews defer until dialog is closed
const allPreviews = useCameraPreviews(
isSearchDialogOpen ? { after: 0, before: 0 } : timeRange,
{
const allPreviews = useCameraPreviews(timeRange, {
camera: selectedCamera ?? undefined,
},
);
fetchPreviews: !isSearchDialogOpen,
});
// ROI state
const [polygonPoints, setPolygonPoints] = useState<number[][]>([]);