diff --git a/src/components/AppHeader.tsx b/src/components/AppHeader.tsx index 20d9de2..c100d06 100644 --- a/src/components/AppHeader.tsx +++ b/src/components/AppHeader.tsx @@ -66,6 +66,7 @@ export function AppHeader() { const [dismissedErrorCode, setDismissedErrorCode] = useState( null, ); + const [wasManuallyDismissed, setWasManuallyDismissed] = useState(false); // Track previous values for comparison const prevMachineError = usePrevious(machineError); @@ -99,7 +100,7 @@ export function AppHeader() { const hadAnyError = prevErrorMessage || prevPyodideError || hasError(prevMachineError); - // Auto-open popover when new error appears + // Auto-open popover when new error appears (but not if user manually dismissed) const isNewMachineError = hasError(machineError) && machineError !== prevMachineError && @@ -108,7 +109,10 @@ export function AppHeader() { machineErrorMessage && machineErrorMessage !== prevErrorMessage; const isNewPyodideError = pyodideError && pyodideError !== prevPyodideError; - if (isNewMachineError || isNewErrorMessage || isNewPyodideError) { + if ( + !wasManuallyDismissed && + (isNewMachineError || isNewErrorMessage || isNewPyodideError) + ) { setErrorPopoverOpen(true); } @@ -116,12 +120,14 @@ export function AppHeader() { if (!hasAnyError && hadAnyError) { setErrorPopoverOpen(false); setDismissedErrorCode(null); // Reset dismissed tracking + setWasManuallyDismissed(false); // Reset manual dismissal flag } }, [ machineError, machineErrorMessage, pyodideError, dismissedErrorCode, + wasManuallyDismissed, prevMachineError, prevErrorMessage, prevPyodideError, @@ -132,9 +138,16 @@ export function AppHeader() { const handlePopoverOpenChange = (open: boolean) => { setErrorPopoverOpen(open); - // If user manually closes it, remember the current error code to prevent reopening - if (!open && hasError(machineError)) { - setDismissedErrorCode(machineError); + // If user manually closes it while any error is present, remember this to prevent reopening + if ( + !open && + (hasError(machineError) || machineErrorMessage || pyodideError) + ) { + setWasManuallyDismissed(true); + // Also track the specific machine error code if present + if (hasError(machineError)) { + setDismissedErrorCode(machineError); + } } }; diff --git a/src/hooks/usePrevious.ts b/src/hooks/usePrevious.ts index d959bb3..26b8021 100644 --- a/src/hooks/usePrevious.ts +++ b/src/hooks/usePrevious.ts @@ -4,28 +4,23 @@ * Returns the previous value of a state or prop * Useful for comparing current vs previous values in effects * - * Note: This uses ref access during render, which is safe for this specific use case. - * The pattern is recommended by React for tracking previous values. + * This implementation updates the ref in an effect instead of during render, + * which is compatible with React Concurrent Mode and Strict Mode. + * + * Note: The ref read on return is safe because it's only returning the previous value + * (from the last render), not the current value being passed in. This is the standard + * pattern for usePrevious hooks. */ -import { useRef } from "react"; +import { useEffect, useRef } from "react"; -/* eslint-disable react-hooks/refs */ export function usePrevious(value: T): T | undefined { - const ref = useRef<{ value: T; prev: T | undefined }>({ - value, - prev: undefined, - }); + const ref = useRef(); - const current = ref.current.value; + useEffect(() => { + ref.current = value; + }, [value]); - if (value !== current) { - ref.current = { - value, - prev: current, - }; - } - - return ref.current.prev; + // eslint-disable-next-line react-hooks/refs + return ref.current; } -/* eslint-enable react-hooks/refs */