Skip to content

Commit 7ecfd34

Browse files
committed
fix(dashboard): stable sidebar nav hydration and Observability loading
- Defer flag-gated category rows until mount + use getFlag ready/on (match Sidebar) - Add Observability block to loading home nav to avoid mount flicker when websites load
1 parent a4183b7 commit 7ecfd34

File tree

5 files changed

+69
-32
lines changed

5 files changed

+69
-32
lines changed

apps/dashboard/components/layout/category-sidebar.tsx

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,15 @@ import {
1414
TooltipContent,
1515
TooltipTrigger,
1616
} from "@/components/ui/tooltip";
17+
import { useHasMounted } from "@/hooks/use-has-mounted";
1718
import { useWebsitesLight } from "@/hooks/use-websites";
1819
import { cn } from "@/lib/utils";
1920
import { Button } from "../ui/button";
2021
import {
2122
categoryConfig,
2223
createLoadingWebsitesNavigation,
2324
createWebsitesNavigation,
25+
filterCategoriesByFlags,
2426
filterCategoriesForRoute,
2527
getContextConfig,
2628
getDefaultCategory,
@@ -54,7 +56,8 @@ export function CategorySidebar({
5456
enabled: user !== null,
5557
});
5658
const [helpOpen, setHelpOpen] = useState(false);
57-
const { isOn } = useFlags();
59+
const { getFlag } = useFlags();
60+
const hasMounted = useHasMounted();
5861
const openCommandSearchAction = useCommandSearchOpenAction();
5962

6063
const { categories, defaultCategory } = useMemo(() => {
@@ -73,18 +76,14 @@ export function CategorySidebar({
7376
: baseConfig;
7477

7578
const defaultCat = getDefaultCategory(pathname);
76-
const filteredCategories = filterCategoriesForRoute(
77-
config.categories,
78-
pathname
79-
).filter((category) => {
80-
if (category.flag && !isOn(category.flag)) {
81-
return false;
82-
}
83-
return true;
84-
});
79+
const filteredCategories = filterCategoriesByFlags(
80+
filterCategoriesForRoute(config.categories, pathname),
81+
hasMounted,
82+
getFlag
83+
);
8584

8685
return { categories: filteredCategories, defaultCategory: defaultCat };
87-
}, [pathname, websites, isLoadingWebsites, isOn]);
86+
}, [pathname, websites, isLoadingWebsites, hasMounted, getFlag]);
8887

8988
const activeCategory = selectedCategory || defaultCategory;
9089

apps/dashboard/components/layout/mobile-sidebar.tsx

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,13 +22,15 @@ import { useCommandSearchOpenAction } from "@/components/ui/command-search";
2222
import { Drawer, DrawerContent } from "@/components/ui/drawer";
2323
import { ScrollArea } from "@/components/ui/scroll-area";
2424
import type { useAccordionStates } from "@/hooks/use-persistent-state";
25+
import { useHasMounted } from "@/hooks/use-has-mounted";
2526
import { useWebsitesLight } from "@/hooks/use-websites";
2627
import { cn } from "@/lib/utils";
2728
import { Branding } from "./logo";
2829
import {
2930
categoryConfig,
3031
createLoadingWebsitesNavigation,
3132
createWebsitesNavigation,
33+
filterCategoriesByFlags,
3234
filterCategoriesForRoute,
3335
getContextConfig,
3436
getDefaultCategory,
@@ -117,7 +119,8 @@ export function MobileSidebar({
117119
const pathname = usePathname();
118120
const router = useRouter();
119121
const openCommandSearchAction = useCommandSearchOpenAction();
120-
const { isOn } = useFlags();
122+
const { getFlag } = useFlags();
123+
const hasMounted = useHasMounted();
121124

122125
const { websites, isLoading: isLoadingWebsites } = useWebsitesLight({
123126
enabled: user !== null,
@@ -142,15 +145,12 @@ export function MobileSidebar({
142145
}
143146
: baseConfig;
144147

145-
return filterCategoriesForRoute(config.categories, pathname).filter(
146-
(category) => {
147-
if (category.flag) {
148-
return isOn(category.flag);
149-
}
150-
return true;
151-
}
148+
return filterCategoriesByFlags(
149+
filterCategoriesForRoute(config.categories, pathname),
150+
hasMounted,
151+
getFlag
152152
);
153-
}, [pathname, websites, isLoadingWebsites, isOn]);
153+
}, [pathname, websites, isLoadingWebsites, hasMounted, getFlag]);
154154

155155
const defaultCategory = useMemo(
156156
() => getDefaultCategory(pathname),

apps/dashboard/components/layout/navigation/mobile-category-selector.tsx

Lines changed: 10 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import { useFlags } from "@databuddy/sdk/react";
55
import { CaretDownIcon } from "@phosphor-icons/react";
66
import { usePathname } from "next/navigation";
77
import { useMemo } from "react";
8+
import { useHasMounted } from "@/hooks/use-has-mounted";
89
import { Button } from "@/components/ui/button";
910
import {
1011
DropdownMenu,
@@ -18,6 +19,7 @@ import {
1819
categoryConfig,
1920
createLoadingWebsitesNavigation,
2021
createWebsitesNavigation,
22+
filterCategoriesByFlags,
2123
filterCategoriesForRoute,
2224
getContextConfig,
2325
getDefaultCategory,
@@ -39,7 +41,8 @@ export function MobileCategorySelector({
3941
const { websites, isLoading: isLoadingWebsites } = useWebsitesLight({
4042
enabled: user !== null,
4143
});
42-
const { isOn } = useFlags();
44+
const { getFlag } = useFlags();
45+
const hasMounted = useHasMounted();
4346

4447
const { categories, defaultCategory } = useMemo(() => {
4548
const baseConfig = getContextConfig(pathname);
@@ -57,19 +60,14 @@ export function MobileCategorySelector({
5760
: baseConfig;
5861

5962
const defaultCat = getDefaultCategory(pathname);
60-
const filteredCategories = filterCategoriesForRoute(
61-
config.categories,
62-
pathname
63-
).filter((category) => {
64-
if (category.flag) {
65-
const flagState = isOn(category.flag);
66-
return flagState;
67-
}
68-
return true;
69-
});
63+
const filteredCategories = filterCategoriesByFlags(
64+
filterCategoriesForRoute(config.categories, pathname),
65+
hasMounted,
66+
getFlag
67+
);
7068

7169
return { categories: filteredCategories, defaultCategory: defaultCat };
72-
}, [pathname, websites, isLoadingWebsites, isOn]);
70+
}, [pathname, websites, isLoadingWebsites, hasMounted, getFlag]);
7371

7472
const activeCategory = selectedCategory || defaultCategory;
7573
const currentCategory = categories.find((cat) => cat.id === activeCategory);

apps/dashboard/components/layout/navigation/navigation-config.tsx

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,28 @@ export const filterCategoriesForRoute = (
7979
return categories.filter((category) => !(category.hideFromDemo && isDemo));
8080
};
8181

82+
/**
83+
* Hides flag-gated categories until the client has mounted, then applies the same
84+
* rule as main navigation: only show when the flag is ready and on. Prevents
85+
* hydration mismatches from `isOn` / flag store differing between SSR and first paint.
86+
*/
87+
export function filterCategoriesByFlags(
88+
categories: Category[],
89+
hasMounted: boolean,
90+
getFlag: (key: string) => { status: string; on: boolean }
91+
): Category[] {
92+
return categories.filter((category) => {
93+
if (!category.flag) {
94+
return true;
95+
}
96+
if (!hasMounted) {
97+
return false;
98+
}
99+
const flagState = getFlag(category.flag);
100+
return flagState.status === "ready" && flagState.on;
101+
});
102+
}
103+
82104
const createDynamicNavigation = <T extends { id: string; name: string | null }>(
83105
items: T[],
84106
title: string,
@@ -480,4 +502,12 @@ export const createLoadingWebsitesNavigation = (): NavigationEntry[] => [
480502
"Loading websites...",
481503
GlobeIcon
482504
),
505+
createNavSection("Observability", ActivityIcon, [
506+
createNavItem("Links", LinkIcon, "/links", {
507+
highlight: true,
508+
}),
509+
createNavItem("Custom Events", LightningIcon, "/events", {
510+
highlight: true,
511+
}),
512+
]),
483513
];
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
import { useEffect, useState } from "react";
2+
3+
/** True after the first client commit. Use to avoid SSR / hydration mismatches for client-only state. */
4+
export function useHasMounted(): boolean {
5+
const [mounted, setMounted] = useState(false);
6+
useEffect(() => {
7+
setMounted(true);
8+
}, []);
9+
return mounted;
10+
}

0 commit comments

Comments
 (0)