mirror of
https://github.com/jhbruhn/respira.git
synced 2026-01-27 10:23:41 +00:00
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
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:
commit
2372278081
2 changed files with 64 additions and 33 deletions
|
|
@ -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
26
src/hooks/usePrevious.ts
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue