From aa60da9cf1b5c8b7461cd75d6b2cfc697d018ae3 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 00:45:39 -0700 Subject: [PATCH] fix: guard settings async state updates (#3532) --- .../src/components/settings/GeneralPane.tsx | 10 ++++++- .../src/components/settings/VoicePane.tsx | 26 ++++++++++++++++--- .../src/lib/windows-terminal-capabilities.ts | 10 ++++++- 3 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/renderer/src/components/settings/GeneralPane.tsx b/src/renderer/src/components/settings/GeneralPane.tsx index 2937d23917e..fc363efccfd 100644 --- a/src/renderer/src/components/settings/GeneralPane.tsx +++ b/src/renderer/src/components/settings/GeneralPane.tsx @@ -128,7 +128,15 @@ export function GeneralPane({ settings, updateSettings }: GeneralPaneProps): Rea >('loading') useEffect(() => { - window.api.updater.getVersion().then(setAppVersion) + let cancelled = false + void window.api.updater.getVersion().then((version) => { + if (!cancelled) { + setAppVersion(version) + } + }) + return () => { + cancelled = true + } }, []) useEffect(() => { diff --git a/src/renderer/src/components/settings/VoicePane.tsx b/src/renderer/src/components/settings/VoicePane.tsx index 1d228e5e09d..dd057108c8e 100644 --- a/src/renderer/src/components/settings/VoicePane.tsx +++ b/src/renderer/src/components/settings/VoicePane.tsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react' +import { useEffect, useRef, useState } from 'react' import type { GlobalSettings } from '../../../../shared/types' import { getDefaultVoiceSettings } from '../../../../shared/constants' import type { SpeechModelManifest, SpeechModelState } from '../../../../shared/speech-types' @@ -32,13 +32,29 @@ export function VoicePane({ settings, updateSettings }: VoicePaneProps): React.J const shortcutLabel = useShortcutLabel('voice.dictation') const [catalog, setCatalog] = useState([]) const [permissionPending, setPermissionPending] = useState(false) + const mountedRef = useRef(true) useEffect(() => { + mountedRef.current = true + return () => { + mountedRef.current = false + } + }, []) + + useEffect(() => { + let cancelled = false refreshModelStates() - window.api.speech + void window.api.speech .getCatalog() - .then(setCatalog) + .then((nextCatalog) => { + if (!cancelled) { + setCatalog(nextCatalog) + } + }) .catch(() => {}) + return () => { + cancelled = true + } }, [refreshModelStates]) useEffect(() => { @@ -84,7 +100,9 @@ export function VoicePane({ settings, updateSettings }: VoicePaneProps): React.J } catch { toast.error('Could not request microphone permission. Voice dictation was not enabled.') } finally { - setPermissionPending(false) + if (mountedRef.current) { + setPermissionPending(false) + } } } diff --git a/src/renderer/src/lib/windows-terminal-capabilities.ts b/src/renderer/src/lib/windows-terminal-capabilities.ts index 51819a22964..caaa59176ad 100644 --- a/src/renderer/src/lib/windows-terminal-capabilities.ts +++ b/src/renderer/src/lib/windows-terminal-capabilities.ts @@ -96,12 +96,20 @@ export function useWindowsTerminalCapabilities( return } + let cancelled = false const cached = getCachedWindowsTerminalCapabilities() setCapabilities(cachedCapabilities ? cached : { ...cached, isLoading: true }) subscribers.add(setCapabilities) - void loadWindowsTerminalCapabilities({ force: forceRefreshOnMount }).then(setCapabilities) + void loadWindowsTerminalCapabilities({ force: forceRefreshOnMount }).then( + (nextCapabilities) => { + if (!cancelled) { + setCapabilities(nextCapabilities) + } + } + ) return () => { + cancelled = true subscribers.delete(setCapabilities) } }, [enabled, forceRefreshOnMount])