fix: wait for zustand hydration before workspace initialization

Fix blank page on first load via `ccw view` by waiting for zustand persist
hydration to complete before initializing workspace.

- Add _hasHydrated state tracking in workflowStore
- Add setHasHydrated action to mark hydration complete
- Update AppShell to wait for hydration before calling switchWorkspace
- Ensures projectPath is properly restored from localStorage before queries execute
This commit is contained in:
catlog22
2026-03-04 10:56:07 +08:00
parent 26bda9c634
commit 5e96722c09
3 changed files with 22 additions and 1 deletions

View File

@@ -40,15 +40,23 @@ export function AppShell({
// Workspace initialization from URL query parameter // Workspace initialization from URL query parameter
const switchWorkspace = useWorkflowStore((state) => state.switchWorkspace); const switchWorkspace = useWorkflowStore((state) => state.switchWorkspace);
const projectPath = useWorkflowStore((state) => state.projectPath); const projectPath = useWorkflowStore((state) => state.projectPath);
const hasHydrated = useWorkflowStore((state) => state._hasHydrated);
const location = useLocation(); const location = useLocation();
// Immersive mode (fullscreen) - hide chrome // Immersive mode (fullscreen) - hide chrome
const isImmersiveMode = useAppStore(selectIsImmersiveMode); const isImmersiveMode = useAppStore(selectIsImmersiveMode);
// Workspace initialization logic (URL > localStorage) // Workspace initialization logic (URL > localStorage)
// Wait for zustand persist hydration to complete before initializing
const [isWorkspaceInitialized, setWorkspaceInitialized] = useState(false); const [isWorkspaceInitialized, setWorkspaceInitialized] = useState(false);
useEffect(() => { useEffect(() => {
// Wait for hydration to complete before initializing workspace
// This ensures projectPath is properly restored from localStorage
if (!hasHydrated) {
return;
}
// This effect should only run once to decide the initial workspace. // This effect should only run once to decide the initial workspace.
if (isWorkspaceInitialized) { if (isWorkspaceInitialized) {
return; return;
@@ -76,7 +84,7 @@ export function AppShell({
// Mark as initialized regardless of whether a path was found. // Mark as initialized regardless of whether a path was found.
setWorkspaceInitialized(true); setWorkspaceInitialized(true);
}, [isWorkspaceInitialized, projectPath, location.search, switchWorkspace]); }, [hasHydrated, isWorkspaceInitialized, projectPath, location.search, switchWorkspace]);
// Sidebar collapse state default to collapsed (hidden) // Sidebar collapse state default to collapsed (hidden)
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => { const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {

View File

@@ -56,6 +56,9 @@ const initialState: WorkflowState = {
// Filters and sorting // Filters and sorting
filters: defaultFilters, filters: defaultFilters,
sorting: defaultSorting, sorting: defaultSorting,
// Hydration state (internal)
_hasHydrated: false,
}; };
export const useWorkflowStore = create<WorkflowStore>()( export const useWorkflowStore = create<WorkflowStore>()(
@@ -414,6 +417,10 @@ export const useWorkflowStore = create<WorkflowStore>()(
set({ _invalidateQueriesCallback: callback }, false, 'registerQueryInvalidator'); set({ _invalidateQueriesCallback: callback }, false, 'registerQueryInvalidator');
}, },
setHasHydrated: (state: boolean) => {
set({ _hasHydrated: state }, false, 'setHasHydrated');
},
// ========== Filters and Sorting ========== // ========== Filters and Sorting ==========
setFilters: (filters: Partial<WorkflowFilters>) => { setFilters: (filters: Partial<WorkflowFilters>) => {
@@ -538,6 +545,8 @@ export const useWorkflowStore = create<WorkflowStore>()(
console.error('[WorkflowStore] Rehydration error:', error); console.error('[WorkflowStore] Rehydration error:', error);
return; return;
} }
// Mark hydration as complete
useWorkflowStore.getState().setHasHydrated(true);
if (state?.projectPath) { if (state?.projectPath) {
if (process.env.NODE_ENV === 'development') { if (process.env.NODE_ENV === 'development') {
// eslint-disable-next-line no-console // eslint-disable-next-line no-console

View File

@@ -335,6 +335,9 @@ export interface WorkflowState {
// Query invalidation callback (internal) // Query invalidation callback (internal)
_invalidateQueriesCallback?: () => void; _invalidateQueriesCallback?: () => void;
// Hydration state (internal)
_hasHydrated: boolean;
} }
export interface WorkflowActions { export interface WorkflowActions {
@@ -369,6 +372,7 @@ export interface WorkflowActions {
removeRecentPath: (path: string) => Promise<void>; removeRecentPath: (path: string) => Promise<void>;
refreshRecentPaths: () => Promise<void>; refreshRecentPaths: () => Promise<void>;
registerQueryInvalidator: (callback: () => void) => void; registerQueryInvalidator: (callback: () => void) => void;
setHasHydrated: (state: boolean) => void;
// Filters and sorting // Filters and sorting
setFilters: (filters: Partial<WorkflowFilters>) => void; setFilters: (filters: Partial<WorkflowFilters>) => void;