From 0d949a3cdae1c82b77529ee5465af238500a986e Mon Sep 17 00:00:00 2001 From: H-Chris233 Date: Thu, 30 Apr 2026 21:21:29 +0800 Subject: [PATCH] Clear frontend timeout callbacks on unmount Store the remaining transient UI timeout handles in refs so Settings About copy feedback and Onboarding delayed permission refresh do not call setState after their components unmount. Constraint: Issue #64 asks for minimal setTimeout handle cleanup without changing UI behavior Confidence: high Scope-risk: narrow Tested: npm run build Tested: git diff --check --- openless-all/app/src/components/Onboarding.tsx | 7 +++++-- openless-all/app/src/pages/Settings.tsx | 10 +++++++++- 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/openless-all/app/src/components/Onboarding.tsx b/openless-all/app/src/components/Onboarding.tsx index b7f8807b..1f272707 100644 --- a/openless-all/app/src/components/Onboarding.tsx +++ b/openless-all/app/src/components/Onboarding.tsx @@ -3,7 +3,7 @@ // 触发条件:App.tsx 启动检查 accessibility + microphone,任一未授权则渲染本组件而非主 Shell。 // 与 Swift `Sources/OpenLessApp/Onboarding/` 同语义,但简化为单页三步。 -import { useEffect, useState } from 'react'; +import { useEffect, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { checkAccessibilityPermission, @@ -25,6 +25,7 @@ export function Onboarding({ onComplete }: OnboardingProps) { const [accessibility, setAccessibility] = useState('notDetermined'); const [microphone, setMicrophone] = useState('notDetermined'); const [busy, setBusy] = useState(false); + const refreshTimeoutRef = useRef | null>(null); const { capability } = useHotkeySettings(); const refresh = async () => { @@ -48,6 +49,7 @@ export function Onboarding({ onComplete }: OnboardingProps) { return () => { window.clearInterval(id); window.removeEventListener('focus', onFocus); + if (refreshTimeoutRef.current) clearTimeout(refreshTimeoutRef.current); }; }, []); @@ -76,7 +78,8 @@ export function Onboarding({ onComplete }: OnboardingProps) { } finally { setBusy(false); } - setTimeout(refresh, 800); + if (refreshTimeoutRef.current) clearTimeout(refreshTimeoutRef.current); + refreshTimeoutRef.current = window.setTimeout(refresh, 800); }; return ( diff --git a/openless-all/app/src/pages/Settings.tsx b/openless-all/app/src/pages/Settings.tsx index ea171387..0474ba6a 100644 --- a/openless-all/app/src/pages/Settings.tsx +++ b/openless-all/app/src/pages/Settings.tsx @@ -735,11 +735,19 @@ function LanguageSection() { function AboutSection() { const { t } = useTranslation(); const [qqCopied, setQqCopied] = useState(false); + const qqCopiedRef = useRef | null>(null); + + useEffect(() => { + return () => { + if (qqCopiedRef.current) clearTimeout(qqCopiedRef.current); + }; + }, []); const copyQq = () => { navigator.clipboard?.writeText('1078960553'); setQqCopied(true); - setTimeout(() => setQqCopied(false), 1500); + if (qqCopiedRef.current) clearTimeout(qqCopiedRef.current); + qqCopiedRef.current = window.setTimeout(() => setQqCopied(false), 1500); }; return (