fix(frontend): resolve URL path inconsistency caused by localStorage race condition

Fixed the issue where accessing the React frontend via ccw view with a URL path
parameter would load a different workspace due to localStorage rehydration race
condition.

Root cause: zustand persist's onRehydrateStorage callback automatically called
switchWorkspace with the cached projectPath before AppShell could process the
URL parameter.

Changes:
- workflowStore.ts: Remove automatic switchWorkspace from onRehydrateStorage
- AppShell.tsx: Centralize initialization logic with clear priority order:
  * Priority 1: URL ?path= parameter (explicit user intent)
  * Priority 2: localStorage fallback (implicit cache)
  * Added isWorkspaceInitialized state lock to prevent duplicate execution

Fixes: URL showing ?path=/new/path but loading /old/path from cache
This commit is contained in:
catlog22
2026-02-01 18:35:51 +08:00
parent d46406df4a
commit 5fb910610a
2 changed files with 37 additions and 21 deletions

View File

@@ -41,22 +41,42 @@ export function AppShell({
const projectPath = useWorkflowStore((state) => state.projectPath);
const location = useLocation();
// Initialize workspace from URL path parameter on mount
// Workspace initialization logic (URL > localStorage)
const [isWorkspaceInitialized, setWorkspaceInitialized] = useState(false);
useEffect(() => {
// Only initialize if no workspace is currently set
if (projectPath) return;
// Read path from URL query parameter
const searchParams = new URLSearchParams(location.search);
const pathParam = searchParams.get('path');
if (pathParam) {
console.log('[AppShell] Initializing workspace from URL:', pathParam);
switchWorkspace(pathParam).catch((error) => {
console.error('[AppShell] Failed to initialize workspace:', error);
});
// This effect should only run once to decide the initial workspace.
if (isWorkspaceInitialized) {
return;
}
}, [location.search, projectPath, switchWorkspace]);
const searchParams = new URLSearchParams(location.search);
const urlPath = searchParams.get('path');
const persistedPath = projectPath; // Path from rehydrated store
let pathFound = false;
// Priority 1: URL parameter.
if (urlPath) {
console.log('[AppShell] Initializing workspace from URL parameter:', urlPath);
switchWorkspace(urlPath).catch((error) => {
console.error('[AppShell] Failed to initialize from URL:', error);
});
pathFound = true;
}
// Priority 2: Rehydrated path from localStorage.
else if (persistedPath) {
console.log('[AppShell] Initializing workspace from persisted state:', persistedPath);
// The path is already in the store, but we need to trigger the data fetch.
switchWorkspace(persistedPath).catch((error) => {
console.error('[AppShell] Failed to re-initialize from persisted state:', error);
});
pathFound = true;
}
// Mark as initialized regardless of whether a path was found.
setWorkspaceInitialized(true);
}, [isWorkspaceInitialized, projectPath, location.search, switchWorkspace]);
// Sidebar collapse state (persisted)
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {

View File

@@ -541,14 +541,10 @@ export const useWorkflowStore = create<WorkflowStore>()(
if (state?.projectPath) {
if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line no-console
console.log('[WorkflowStore] Found persisted projectPath, re-initializing workspace:', state.projectPath);
console.log('[WorkflowStore] Rehydrated with persisted projectPath:', state.projectPath);
}
// Use setTimeout to ensure the store is fully initialized before calling switchWorkspace
setTimeout(() => {
if (state.switchWorkspace) {
state.switchWorkspace(state.projectPath);
}
}, 0);
// The initialization logic is now handled by AppShell.tsx
// to correctly prioritize URL parameters over localStorage.
}
};
},