From 58efc66ca71df9f91d610ee32ab2021467c06477 Mon Sep 17 00:00:00 2001 From: byGalax Date: Sun, 17 May 2026 01:03:37 +0200 Subject: [PATCH] fix(P4A): guard ImageAnnotator load callbacks with cancelled flag (React strict double-mount) --- apps/desktop/src/components/ImageAnnotator.tsx | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/apps/desktop/src/components/ImageAnnotator.tsx b/apps/desktop/src/components/ImageAnnotator.tsx index 956f44c..d734492 100644 --- a/apps/desktop/src/components/ImageAnnotator.tsx +++ b/apps/desktop/src/components/ImageAnnotator.tsx @@ -51,18 +51,31 @@ export function ImageAnnotator({ file, onCancel, onSave }: Props) { useEffect(() => { onCancelRef.current = onCancel; }, [onCancel]); useEffect(() => { + // React 18 strict mode in dev double-mounts effects to test idempotency. + // The first run creates a blob URL, sets img.src, returns a cleanup + // that revokes — and the cleanup fires BEFORE the (still-in-flight) + // image fetch completes. The browser then emits ERR_FILE_NOT_FOUND for + // the revoked URL → img.onerror → modal closes instantly. The + // `cancelled` flag guards every callback so a torn-down run can't + // close the modal that the second mount just opened. + let cancelled = false; const url = URL.createObjectURL(file); const img = new Image(); img.onload = () => { + if (cancelled) return; imageRef.current = img; setImageLoaded(true); }; img.onerror = () => { + if (cancelled) return; console.error('ImageAnnotator: failed to decode source image'); onCancelRef.current(); }; img.src = url; - return () => URL.revokeObjectURL(url); + return () => { + cancelled = true; + URL.revokeObjectURL(url); + }; }, [file]); useEffect(() => {