From 1be9c56ec58269cd91a736aa769fa611ad160be9 Mon Sep 17 00:00:00 2001 From: Jason Date: Mon, 29 Dec 2025 22:14:34 +0800 Subject: [PATCH] fix(ui): resolve Dialog/Modal not opening on first click Two bugs were caused by ref synchronization race condition in commit 7d495aa: - EditProviderDialog: provider prop was null on first render - UsageScriptModal: conditional render guard was false on first click Root cause: useEffect updates ref asynchronously, but render happens before effect runs. On first click, ref is still null causing components to fail. Solution: Create useLastValidValue hook that updates ref synchronously during render phase instead of in useEffect. This ensures ref is always in sync with state, eliminating the race condition. Changes: - Add useLastValidValue hook for preserving last valid value during animations - Replace manual ref + useEffect pattern with the new hook - Remove non-null assertions (!) that were needed as workaround --- src/App.tsx | 25 +++++++------------------ src/hooks/useLastValidValue.ts | 20 ++++++++++++++++++++ 2 files changed, 27 insertions(+), 18 deletions(-) create mode 100644 src/hooks/useLastValidValue.ts diff --git a/src/App.tsx b/src/App.tsx index d8bb32c4d..5eaeda9bd 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -26,6 +26,7 @@ import { import { checkAllEnvConflicts, checkEnvConflicts } from "@/lib/api/env"; import { useProviderActions } from "@/hooks/useProviderActions"; import { useProxyStatus } from "@/hooks/useProxyStatus"; +import { useLastValidValue } from "@/hooks/useLastValidValue"; import { extractErrorMessage } from "@/utils/errorUtils"; import { cn } from "@/lib/utils"; import { AppSwitcher } from "@/components/AppSwitcher"; @@ -73,21 +74,9 @@ function App() { const [envConflicts, setEnvConflicts] = useState([]); const [showEnvBanner, setShowEnvBanner] = useState(false); - // 保存最后一个有效的 provider,用于动画退出期间显示内容 - const lastUsageProviderRef = useRef(null); - const lastEditingProviderRef = useRef(null); - - useEffect(() => { - if (usageProvider) { - lastUsageProviderRef.current = usageProvider; - } - }, [usageProvider]); - - useEffect(() => { - if (editingProvider) { - lastEditingProviderRef.current = editingProvider; - } - }, [editingProvider]); + // 使用 Hook 保存最后有效值,用于动画退出期间保持内容显示 + const effectiveEditingProvider = useLastValidValue(editingProvider); + const effectiveUsageProvider = useLastValidValue(usageProvider); const promptPanelRef = useRef(null); const mcpPanelRef = useRef(null); @@ -718,7 +707,7 @@ function App() { { if (!open) { setEditingProvider(null); @@ -729,9 +718,9 @@ function App() { isProxyTakeover={isProxyRunning && isCurrentAppTakeoverActive} /> - {lastUsageProviderRef.current && ( + {effectiveUsageProvider && ( setUsageProvider(null)} diff --git a/src/hooks/useLastValidValue.ts b/src/hooks/useLastValidValue.ts new file mode 100644 index 000000000..22b3ae405 --- /dev/null +++ b/src/hooks/useLastValidValue.ts @@ -0,0 +1,20 @@ +import { useRef } from "react"; + +/** + * 保存最后一个非 null/undefined 的值 + * 用于 Dialog 关闭动画期间保持内容显示 + * + * @param value 当前值 + * @returns 当前值(如果有效)或最后一个有效值 + */ +export function useLastValidValue(value: T | null | undefined): T | null { + const ref = useRef(null); + + // 同步更新 ref(在渲染期间,不在 useEffect 中) + if (value != null) { + ref.current = value; + } + + // 返回当前值或最后有效值 + return value ?? ref.current; +}