From 7bb0a8896824dc21a769e5f706bbbcc996f841cd Mon Sep 17 00:00:00 2001 From: Felix201209 Date: Sun, 21 Jun 2026 08:17:10 -0700 Subject: [PATCH] fix(ui): unlisten Tauri events when unmounted before listen() resolves MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three useEffect cleanups stored the unlisten handle with `if (!cancelled) unlisten = fn`. When the component unmounts before the async `listen()` promise resolves, `cancelled` is already true, so the handle is dropped on the floor — the underlying Tauri subscription was created but is never torn down. The leaked listener keeps firing (and calling setState) on an unmounted component. Align these three sites with the repo's own established convention (Capsule.tsx / Vocab.tsx / LessComputerPanel.tsx / LessComputerGlow.tsx): `if (cancelled) fn(); else unlisten = fn;` so an already-unmounted effect immediately unsubscribes. - AudioCue.tsx: `prefs:changed` and `capsule:state` listeners - WindowChrome.tsx: LinuxTitlebar `tauri://resize` listener Verified: `tsc --noEmit` passes. Co-Authored-By: Claude Opus 4.8 --- openless-all/app/src/components/AudioCue.tsx | 4 ++-- openless-all/app/src/components/WindowChrome.tsx | 3 ++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/openless-all/app/src/components/AudioCue.tsx b/openless-all/app/src/components/AudioCue.tsx index e3f5f4ad..128f2647 100644 --- a/openless-all/app/src/components/AudioCue.tsx +++ b/openless-all/app/src/components/AudioCue.tsx @@ -46,7 +46,7 @@ export function AudioCueListener() { listen('prefs:changed', (event) => { const next = event.payload; if (next) audioCueEnabledRef.current = next.audioCueOnRecord !== false; - }).then(fn => { if (!cancelled) unlisten = fn; }).catch(() => {}); + }).then(fn => { if (cancelled) fn(); else unlisten = fn; }).catch(() => {}); })(); return () => { cancelled = true; unlisten?.(); }; }, [audioCueRuntimeEnabled]); @@ -73,7 +73,7 @@ export function AudioCueListener() { } else if (state !== 'recording' && prev === 'recording') { stopAudioCue(); } - }).then(fn => { if (!cancelled) unlisten = fn; }).catch(() => {}); + }).then(fn => { if (cancelled) fn(); else unlisten = fn; }).catch(() => {}); })(); return () => { cancelled = true; unlisten?.(); }; }, [audioCueRuntimeEnabled]); diff --git a/openless-all/app/src/components/WindowChrome.tsx b/openless-all/app/src/components/WindowChrome.tsx index a6e6c6e4..0c8fa084 100644 --- a/openless-all/app/src/components/WindowChrome.tsx +++ b/openless-all/app/src/components/WindowChrome.tsx @@ -113,7 +113,8 @@ function LinuxTitlebar() { if (!cancelled) setMaximized(m); }).catch(() => {}); }).then((fn) => { - if (!cancelled) unlisten = fn; + if (cancelled) fn(); + else unlisten = fn; }).catch(() => {}); }).catch(() => {}); return () => {