Merge pull request #54 from jhbruhn/refactor/36-improve-useeffect-with-useprevious
Some checks are pending
Build, Test, and Lint / Build, Test, and Lint (push) Waiting to run
Draft Release / Draft Release (push) Waiting to run
Draft Release / Build Web App (push) Blocked by required conditions
Draft Release / Build Release - macos-latest (push) Blocked by required conditions
Draft Release / Build Release - ubuntu-latest (push) Blocked by required conditions
Draft Release / Build Release - windows-latest (push) Blocked by required conditions
Draft Release / Upload to GitHub Release (push) Blocked by required conditions

refactor: Improve useEffect patterns in AppHeader with usePrevious hook
This commit is contained in:
Jan-Henrik Bruhn 2025-12-27 12:00:58 +01:00 committed by GitHub
commit 2372278081
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 64 additions and 33 deletions

View file

@ -1,7 +1,8 @@
import { useState, useEffect, useRef } from "react"; import { useState, useEffect } from "react";
import { useShallow } from "zustand/react/shallow"; import { useShallow } from "zustand/react/shallow";
import { useMachineStore } from "../stores/useMachineStore"; import { useMachineStore } from "../stores/useMachineStore";
import { useUIStore } from "../stores/useUIStore"; import { useUIStore } from "../stores/useUIStore";
import { usePrevious } from "../hooks/usePrevious";
import { WorkflowStepper } from "./WorkflowStepper"; import { WorkflowStepper } from "./WorkflowStepper";
import { ErrorPopoverContent } from "./ErrorPopover"; import { ErrorPopoverContent } from "./ErrorPopover";
import { import {
@ -65,9 +66,12 @@ export function AppHeader() {
const [dismissedErrorCode, setDismissedErrorCode] = useState<number | null>( const [dismissedErrorCode, setDismissedErrorCode] = useState<number | null>(
null, null,
); );
const prevMachineErrorRef = useRef<number | undefined>(undefined); const [wasManuallyDismissed, setWasManuallyDismissed] = useState(false);
const prevErrorMessageRef = useRef<string | null>(null);
const prevPyodideErrorRef = useRef<string | null>(null); // Track previous values for comparison
const prevMachineError = usePrevious(machineError);
const prevErrorMessage = usePrevious(machineErrorMessage);
const prevPyodideError = usePrevious(pyodideError);
// Get state visual info for header status badge // Get state visual info for header status badge
const stateVisual = getStateVisualInfo(machineStatus); const stateVisual = getStateVisualInfo(machineStatus);
@ -89,31 +93,26 @@ export function AppHeader() {
// Auto-open/close error popover based on error state changes // Auto-open/close error popover based on error state changes
/* eslint-disable react-hooks/set-state-in-effect */ /* eslint-disable react-hooks/set-state-in-effect */
useEffect(() => { useEffect(() => {
const currentError = machineError;
const prevError = prevMachineErrorRef.current;
const currentErrorMessage = machineErrorMessage;
const prevErrorMessage = prevErrorMessageRef.current;
const currentPyodideError = pyodideError;
const prevPyodideError = prevPyodideErrorRef.current;
// Check if there's any error now // Check if there's any error now
const hasAnyError = const hasAnyError =
machineErrorMessage || pyodideError || hasError(currentError); machineErrorMessage || pyodideError || hasError(machineError);
// Check if there was any error before // Check if there was any error before
const hadAnyError = const hadAnyError =
prevErrorMessage || prevPyodideError || hasError(prevError); 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 = const isNewMachineError =
hasError(currentError) && hasError(machineError) &&
currentError !== prevError && machineError !== prevMachineError &&
currentError !== dismissedErrorCode; machineError !== dismissedErrorCode;
const isNewErrorMessage = const isNewErrorMessage =
currentErrorMessage && currentErrorMessage !== prevErrorMessage; machineErrorMessage && machineErrorMessage !== prevErrorMessage;
const isNewPyodideError = const isNewPyodideError = pyodideError && pyodideError !== prevPyodideError;
currentPyodideError && currentPyodideError !== prevPyodideError;
if (isNewMachineError || isNewErrorMessage || isNewPyodideError) { if (
!wasManuallyDismissed &&
(isNewMachineError || isNewErrorMessage || isNewPyodideError)
) {
setErrorPopoverOpen(true); setErrorPopoverOpen(true);
} }
@ -121,28 +120,34 @@ export function AppHeader() {
if (!hasAnyError && hadAnyError) { if (!hasAnyError && hadAnyError) {
setErrorPopoverOpen(false); setErrorPopoverOpen(false);
setDismissedErrorCode(null); // Reset dismissed tracking setDismissedErrorCode(null); // Reset dismissed tracking
setWasManuallyDismissed(false); // Reset manual dismissal flag
} }
}, [
// Update refs for next comparison machineError,
prevMachineErrorRef.current = currentError; machineErrorMessage,
prevErrorMessageRef.current = currentErrorMessage; pyodideError,
prevPyodideErrorRef.current = currentPyodideError; dismissedErrorCode,
}, [machineError, machineErrorMessage, pyodideError, dismissedErrorCode]); wasManuallyDismissed,
prevMachineError,
prevErrorMessage,
prevPyodideError,
]);
/* eslint-enable react-hooks/set-state-in-effect */ /* eslint-enable react-hooks/set-state-in-effect */
// Handle manual popover dismiss // Handle manual popover dismiss
const handlePopoverOpenChange = (open: boolean) => { const handlePopoverOpenChange = (open: boolean) => {
setErrorPopoverOpen(open); setErrorPopoverOpen(open);
// If user manually closes it, remember the current error state to prevent reopening // If user manually closes it while any error is present, remember this to prevent reopening
if (!open) { if (
// For machine errors, track the error code !open &&
(hasError(machineError) || machineErrorMessage || pyodideError)
) {
setWasManuallyDismissed(true);
// Also track the specific machine error code if present
if (hasError(machineError)) { if (hasError(machineError)) {
setDismissedErrorCode(machineError); setDismissedErrorCode(machineError);
} }
// Update refs so we don't reopen for the same error message/pyodide error
prevErrorMessageRef.current = machineErrorMessage;
prevPyodideErrorRef.current = pyodideError;
} }
}; };

26
src/hooks/usePrevious.ts Normal file
View file

@ -0,0 +1,26 @@
/**
* usePrevious Hook
*
* Returns the previous value of a state or prop
* Useful for comparing current vs previous values in effects
*
* 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 { useEffect, useRef } from "react";
export function usePrevious<T>(value: T): T | undefined {
const ref = useRef<T | undefined>(undefined);
useEffect(() => {
ref.current = value;
}, [value]);
// eslint-disable-next-line react-hooks/refs
return ref.current;
}