From 1e139fb86e13e0e3273f3046e91d1f063df79e6e Mon Sep 17 00:00:00 2001 From: byGalax Date: Sun, 17 May 2026 01:01:27 +0200 Subject: [PATCH] =?UTF-8?q?fix(P4A):=20ImageAnnotator=20effect-race=20?= =?UTF-8?q?=E2=80=94=20stabilize=20onCancel=20via=20ref=20so=20URL.revoke?= =?UTF-8?q?=20doesn't=20fire=20mid-decode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- apps/desktop/src/components/ImageAnnotator.tsx | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/apps/desktop/src/components/ImageAnnotator.tsx b/apps/desktop/src/components/ImageAnnotator.tsx index a42abfc..956f44c 100644 --- a/apps/desktop/src/components/ImageAnnotator.tsx +++ b/apps/desktop/src/components/ImageAnnotator.tsx @@ -41,6 +41,15 @@ export function ImageAnnotator({ file, onCancel, onSave }: Props) { const draftRef = useRef(null); const [draftTick, setDraftTick] = useState(0); + // Hold a stable ref to onCancel so the image-load effect doesn't depend + // on its identity. Without this, parents that pass an inline `() => …` + // re-render the modal on every keystroke / state change, the effect re- + // runs, the previous URL.createObjectURL gets revoked WHILE the new img + // is still decoding → img.onerror fires ("file not found") → onCancel → + // modal flashes open + closes instantly. + const onCancelRef = useRef(onCancel); + useEffect(() => { onCancelRef.current = onCancel; }, [onCancel]); + useEffect(() => { const url = URL.createObjectURL(file); const img = new Image(); @@ -50,11 +59,11 @@ export function ImageAnnotator({ file, onCancel, onSave }: Props) { }; img.onerror = () => { console.error('ImageAnnotator: failed to decode source image'); - onCancel(); + onCancelRef.current(); }; img.src = url; return () => URL.revokeObjectURL(url); - }, [file, onCancel]); + }, [file]); useEffect(() => { if (!imageLoaded) return;