Skip to content

Commit 5e96722

Browse files
author
catlog22
committed
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
1 parent 26bda9c commit 5e96722

3 files changed

Lines changed: 22 additions & 1 deletion

File tree

ccw/frontend/src/components/layout/AppShell.tsx

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,23 @@ export function AppShell({
4040
// Workspace initialization from URL query parameter
4141
const switchWorkspace = useWorkflowStore((state) => state.switchWorkspace);
4242
const projectPath = useWorkflowStore((state) => state.projectPath);
43+
const hasHydrated = useWorkflowStore((state) => state._hasHydrated);
4344
const location = useLocation();
4445

4546
// Immersive mode (fullscreen) - hide chrome
4647
const isImmersiveMode = useAppStore(selectIsImmersiveMode);
4748

4849
// Workspace initialization logic (URL > localStorage)
50+
// Wait for zustand persist hydration to complete before initializing
4951
const [isWorkspaceInitialized, setWorkspaceInitialized] = useState(false);
5052

5153
useEffect(() => {
54+
// Wait for hydration to complete before initializing workspace
55+
// This ensures projectPath is properly restored from localStorage
56+
if (!hasHydrated) {
57+
return;
58+
}
59+
5260
// This effect should only run once to decide the initial workspace.
5361
if (isWorkspaceInitialized) {
5462
return;
@@ -76,7 +84,7 @@ export function AppShell({
7684

7785
// Mark as initialized regardless of whether a path was found.
7886
setWorkspaceInitialized(true);
79-
}, [isWorkspaceInitialized, projectPath, location.search, switchWorkspace]);
87+
}, [hasHydrated, isWorkspaceInitialized, projectPath, location.search, switchWorkspace]);
8088

8189
// Sidebar collapse state – default to collapsed (hidden)
8290
const [sidebarCollapsed, setSidebarCollapsed] = useState(() => {

ccw/frontend/src/stores/workflowStore.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,9 @@ const initialState: WorkflowState = {
5656
// Filters and sorting
5757
filters: defaultFilters,
5858
sorting: defaultSorting,
59+
60+
// Hydration state (internal)
61+
_hasHydrated: false,
5962
};
6063

6164
export const useWorkflowStore = create<WorkflowStore>()(
@@ -414,6 +417,10 @@ export const useWorkflowStore = create<WorkflowStore>()(
414417
set({ _invalidateQueriesCallback: callback }, false, 'registerQueryInvalidator');
415418
},
416419

420+
setHasHydrated: (state: boolean) => {
421+
set({ _hasHydrated: state }, false, 'setHasHydrated');
422+
},
423+
417424
// ========== Filters and Sorting ==========
418425

419426
setFilters: (filters: Partial<WorkflowFilters>) => {
@@ -538,6 +545,8 @@ export const useWorkflowStore = create<WorkflowStore>()(
538545
console.error('[WorkflowStore] Rehydration error:', error);
539546
return;
540547
}
548+
// Mark hydration as complete
549+
useWorkflowStore.getState().setHasHydrated(true);
541550
if (state?.projectPath) {
542551
if (process.env.NODE_ENV === 'development') {
543552
// eslint-disable-next-line no-console

ccw/frontend/src/types/store.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,6 +335,9 @@ export interface WorkflowState {
335335

336336
// Query invalidation callback (internal)
337337
_invalidateQueriesCallback?: () => void;
338+
339+
// Hydration state (internal)
340+
_hasHydrated: boolean;
338341
}
339342

340343
export interface WorkflowActions {
@@ -369,6 +372,7 @@ export interface WorkflowActions {
369372
removeRecentPath: (path: string) => Promise<void>;
370373
refreshRecentPaths: () => Promise<void>;
371374
registerQueryInvalidator: (callback: () => void) => void;
375+
setHasHydrated: (state: boolean) => void;
372376

373377
// Filters and sorting
374378
setFilters: (filters: Partial<WorkflowFilters>) => void;

0 commit comments

Comments
 (0)