From 3fd0c6981de9455deabbc5901c064934510498b9 Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Mon, 14 Sep 2026 08:25:35 +0200 Subject: [PATCH] =?UTF-8?q?fix(app-shell):=20instabile=20Zustands-Selektor?= =?UTF-8?q?en=20veroursachten=20haengende=20Lazy-Routen=20("Dashboard=20lo?= =?UTF-8?q?ads=20forever")=20=E2=80=94=20Root=20Cause:=20moduleMenuOrder()?= =?UTF-8?q?/visibleModuleKeys()=20erzeugten=20bei=20jedem=20getSnapshot=20?= =?UTF-8?q?neue=20Map/Set-Objekte?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/components/layout/Sidebar.tsx | 11 ++++++++++- frontend/src/hooks/useWorkspace.ts | 10 ++++++++-- frontend/src/routes/index.tsx | 2 -- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/frontend/src/components/layout/Sidebar.tsx b/frontend/src/components/layout/Sidebar.tsx index cb34a6a..4ecb7eb 100644 --- a/frontend/src/components/layout/Sidebar.tsx +++ b/frontend/src/components/layout/Sidebar.tsx @@ -73,7 +73,16 @@ export function Sidebar() { const { hasPermission } = usePermission(); const user = useAuthStore((state) => state.user); const { isModuleVisible } = useWorkspace(); - const moduleMenuOrder = useWorkspaceStore(s => s.moduleMenuOrder()); + // NOTE: never select object-factory calls in a Zustand selector — + // `s.moduleMenuOrder()` creates a NEW Map on every getSnapshot call, + // which destabilizes useSyncExternalStore and causes a synchronous + // re-render loop that starves pending Suspense (lazy route) commits + // ("Dashboard loads forever" root cause). Select stable state and + // derive with useMemo instead. + const workspaceContext = useWorkspaceStore(s => s.context); + const moduleMenuOrder = useMemo(() => new Map( + (workspaceContext?.modules ?? []).map(m => [m.module_key, m.menu_order] as const), + ), [workspaceContext]); // Use hasPermission directly — permissions are loaded via useUserPermissions hook const canAccess = (perm?: string): boolean => { diff --git a/frontend/src/hooks/useWorkspace.ts b/frontend/src/hooks/useWorkspace.ts index 9907f3e..27bd781 100644 --- a/frontend/src/hooks/useWorkspace.ts +++ b/frontend/src/hooks/useWorkspace.ts @@ -1,4 +1,4 @@ -import { useEffect, useCallback } from 'react'; +import { useEffect, useCallback, useMemo } from 'react'; import { useMyWorkspaces, useWorkspaceContext } from '@/api/hooks/workspaces'; import { useWorkspaceStore, type WorkspaceInfo, type WorkspaceContextState } from '@/store/workspaceStore'; import { useAuthStore } from '@/store/authStore'; @@ -62,7 +62,13 @@ export function useWorkspace() { return isModuleVisibleFromStore(moduleKey, isSystemAdmin); }, [isModuleVisibleFromStore, isSystemAdmin]); - const visibleModuleKeys = useWorkspaceStore(s => s.visibleModuleKeys()); + // NOTE: derive from the stable `context` reference via useMemo — + // `s.visibleModuleKeys()` would create a NEW Set on every getSnapshot + // call, destabilizing useSyncExternalStore (same root cause as the + // Sidebar moduleMenuOrder infinite re-render loop). + const visibleModuleKeys = useMemo(() => new Set( + (context?.modules ?? []).filter(m => m.is_visible !== false).map(m => m.module_key), + ), [context]); return { activeWorkspaceId, diff --git a/frontend/src/routes/index.tsx b/frontend/src/routes/index.tsx index 57663f2..a2c510a 100644 --- a/frontend/src/routes/index.tsx +++ b/frontend/src/routes/index.tsx @@ -94,8 +94,6 @@ const router = createBrowserRouter([ path: '/login', element: , }, - { - }, { path: '/dms-standalone', element: {withSuspense()},