From b7a625e0f2b34a469ee5782162f9ab1159cdac39 Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sat, 30 May 2026 02:27:20 -0700 Subject: [PATCH] fix: guard sparse preset async state (#3572) --- .../settings/SparsePresetSettingsSection.tsx | 8 ++++++-- .../sparse/SparseCheckoutPresetSelect.tsx | 9 +++++++-- src/renderer/src/hooks/useMountedRef.ts | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 4 deletions(-) create mode 100644 src/renderer/src/hooks/useMountedRef.ts diff --git a/src/renderer/src/components/settings/SparsePresetSettingsSection.tsx b/src/renderer/src/components/settings/SparsePresetSettingsSection.tsx index 4ff33ffc7d7..06db63b3526 100644 --- a/src/renderer/src/components/settings/SparsePresetSettingsSection.tsx +++ b/src/renderer/src/components/settings/SparsePresetSettingsSection.tsx @@ -4,6 +4,7 @@ import type { SparsePreset } from '../../../../shared/types' import { useAppStore } from '../../store' import { cn } from '@/lib/utils' import { parseSparsePresetDirectories } from '@/lib/sparse-preset-draft' +import { useMountedRef } from '@/hooks/useMountedRef' import { Button } from '../ui/button' import { Input } from '../ui/input' import { Label } from '../ui/label' @@ -59,6 +60,7 @@ export function SparsePresetSettingsSection({ const [draft, setDraft] = useState(null) const [submitting, setSubmitting] = useState(false) const [confirmingDeleteId, setConfirmingDeleteId] = useState(null) + const mountedRef = useMountedRef() useEffect(() => { if (presets === undefined) { @@ -119,11 +121,13 @@ export function SparsePresetSettingsSection({ name: trimmedName, directories: parsedDirectories.directories }) - if (saved) { + if (saved && mountedRef.current) { setDraft(null) } } finally { - setSubmitting(false) + if (mountedRef.current) { + setSubmitting(false) + } } } diff --git a/src/renderer/src/components/sparse/SparseCheckoutPresetSelect.tsx b/src/renderer/src/components/sparse/SparseCheckoutPresetSelect.tsx index f58de9f5013..7561d082213 100644 --- a/src/renderer/src/components/sparse/SparseCheckoutPresetSelect.tsx +++ b/src/renderer/src/components/sparse/SparseCheckoutPresetSelect.tsx @@ -5,6 +5,7 @@ import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover import { useAppStore } from '@/store' import { cn } from '@/lib/utils' import { parseSparsePresetDirectories } from '@/lib/sparse-preset-draft' +import { useMountedRef } from '@/hooks/useMountedRef' import type { SparsePreset } from '../../../../shared/types' type SparseCheckoutPresetSelectProps = { @@ -41,6 +42,7 @@ export default function SparseCheckoutPresetSelect({ const [submitting, setSubmitting] = useState(false) const nameInputRef = useRef(null) const nameInputFocusFrameRef = useRef(null) + const mountedRef = useMountedRef() const visiblePresets = presetsForRepo ?? presets const presetsLoaded = presetsForRepo !== undefined @@ -147,7 +149,7 @@ export default function SparseCheckoutPresetSelect({ name: trimmedName, directories: parsedDirectories.directories }) - if (saved) { + if (saved && mountedRef.current) { if (draft.mode === 'new' || selectedPresetId === saved.id) { onSelectPreset(saved) } @@ -155,11 +157,14 @@ export default function SparseCheckoutPresetSelect({ setOpen(false) } } finally { - setSubmitting(false) + if (mountedRef.current) { + setSubmitting(false) + } } }, [ canSave, draft, + mountedRef, onSelectPreset, parsedDirectories, repoId, diff --git a/src/renderer/src/hooks/useMountedRef.ts b/src/renderer/src/hooks/useMountedRef.ts new file mode 100644 index 00000000000..cdc17d5f918 --- /dev/null +++ b/src/renderer/src/hooks/useMountedRef.ts @@ -0,0 +1,14 @@ +import { useEffect, useRef, type MutableRefObject } from 'react' + +export function useMountedRef(): MutableRefObject { + const mountedRef = useRef(true) + + useEffect(() => { + mountedRef.current = true + return () => { + mountedRef.current = false + } + }, []) + + return mountedRef +}