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;