fix(dashboard): show skeleton instead of default layout until hydration completes

Root cause: useDashboardLayout initialised React state with createDefaultDashboardLayout()
(1 widget), so the wrong default rendered during the ~100–500ms window while React Query
fetched the user session and DB layout after login. On reload within staleTime the cache
hit resolved instantly, masking the bug.

Fix: add isHydrated boolean state that becomes true only once localStorage OR DB
hydration has settled; DashboardClient renders a GridLayoutSkeleton until then.
Also adds router.refresh() in the sign-in handler to bust the Next.js Router Cache
so the post-login navigation always lands on a fresh server component tree.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-09 10:20:50 +02:00
parent 24435a1824
commit a16c41e739
3 changed files with 69 additions and 10 deletions
@@ -1,6 +1,7 @@
"use client";
import type { DashboardWidgetConfig, DashboardWidgetType } from "@capakraken/shared/types";
import { noCompactor } from "react-grid-layout";
import dynamic from "next/dynamic";
import { Suspense, useState, useRef, useEffect, useMemo } from "react";
import { useDashboardLayout } from "~/hooks/useDashboardLayout.js";
@@ -145,7 +146,7 @@ function DeferredWidgetBody({
export function DashboardClient() {
const [addModalOpen, setAddModalOpen] = useState(false);
const { config, addWidget, removeWidget, updateWidgetConfig, onLayoutChange, resetLayout } =
const { config, isHydrated, addWidget, removeWidget, updateWidgetConfig, onLayoutChange, resetLayout } =
useDashboardLayout();
// Measure grid container width so Responsive knows the column size.
@@ -226,6 +227,26 @@ export function DashboardClient() {
[config.widgets, removeWidget, updateWidgetConfig],
);
// Show a skeleton while hydration is in-flight (avoids flashing the 1-widget default
// layout before the user's real layout is loaded from localStorage or the DB).
if (!isHydrated) {
return (
<div className="app-page space-y-6">
<div className="app-page-header gap-4">
<div>
<h1 className="app-page-title">Dashboard</h1>
<p className="app-page-subtitle mt-1">
Drag widgets to rearrange them and resize from the corners.
</p>
</div>
</div>
<div className="app-surface overflow-hidden p-3">
<GridLayoutSkeleton />
</div>
</div>
);
}
return (
<div className="app-page space-y-6">
<div className="app-page-header gap-4">
@@ -291,8 +312,7 @@ export function DashboardClient() {
breakpoints={{ lg: 1200, md: 996, sm: 768, xs: 480, xxs: 0 }}
cols={{ lg: 12, md: 10, sm: 6, xs: 4, xxs: 2 }}
rowHeight={80}
compactType={null}
preventCollision={false}
compactor={noCompactor}
onLayoutChange={(
_: unknown,
allLayouts: Record<