Fix sidebar hydration flash
This commit is contained in:
@@ -1,6 +1,12 @@
|
|||||||
import './globals.css';
|
import './globals.css';
|
||||||
import type { Metadata, Viewport } from 'next';
|
import type { Metadata, Viewport } from 'next';
|
||||||
|
import { cookies } from 'next/headers';
|
||||||
|
import { SidebarPreferenceProvider } from '@/components/providers/sidebar-preference-provider';
|
||||||
import { QueryProvider } from '@/components/providers/query-provider';
|
import { QueryProvider } from '@/components/providers/query-provider';
|
||||||
|
import {
|
||||||
|
SIDEBAR_PREFERENCE_KEY,
|
||||||
|
parseSidebarPreference
|
||||||
|
} from '@/lib/sidebar-preference';
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: 'Fiscal Clone',
|
title: 'Fiscal Clone',
|
||||||
@@ -14,11 +20,18 @@ export const viewport: Viewport = {
|
|||||||
themeColor: '#121417'
|
themeColor: '#121417'
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RootLayout({ children }: { children: React.ReactNode }) {
|
export default async function RootLayout({ children }: { children: React.ReactNode }) {
|
||||||
|
const cookieStore = await cookies();
|
||||||
|
const initialSidebarCollapsed = parseSidebarPreference(
|
||||||
|
cookieStore.get(SIDEBAR_PREFERENCE_KEY)?.value
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
<body>
|
<body>
|
||||||
<QueryProvider>{children}</QueryProvider>
|
<SidebarPreferenceProvider initialSidebarCollapsed={initialSidebarCollapsed}>
|
||||||
|
<QueryProvider>{children}</QueryProvider>
|
||||||
|
</SidebarPreferenceProvider>
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
);
|
);
|
||||||
|
|||||||
32
components/providers/sidebar-preference-provider.tsx
Normal file
32
components/providers/sidebar-preference-provider.tsx
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
'use client';
|
||||||
|
|
||||||
|
import { createContext, useContext } from 'react';
|
||||||
|
|
||||||
|
type SidebarPreferenceContextValue = {
|
||||||
|
initialSidebarCollapsed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SidebarPreferenceContext =
|
||||||
|
createContext<SidebarPreferenceContextValue>({
|
||||||
|
initialSidebarCollapsed: false
|
||||||
|
});
|
||||||
|
|
||||||
|
type SidebarPreferenceProviderProps = {
|
||||||
|
children: React.ReactNode;
|
||||||
|
initialSidebarCollapsed: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export function SidebarPreferenceProvider({
|
||||||
|
children,
|
||||||
|
initialSidebarCollapsed
|
||||||
|
}: SidebarPreferenceProviderProps) {
|
||||||
|
return (
|
||||||
|
<SidebarPreferenceContext.Provider value={{ initialSidebarCollapsed }}>
|
||||||
|
{children}
|
||||||
|
</SidebarPreferenceContext.Provider>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useSidebarPreference() {
|
||||||
|
return useContext(SidebarPreferenceContext);
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ import { useEffect, useMemo, useState } from "react";
|
|||||||
import { authClient } from "@/lib/auth-client";
|
import { authClient } from "@/lib/auth-client";
|
||||||
import { TaskDetailModal } from "@/components/notifications/task-detail-modal";
|
import { TaskDetailModal } from "@/components/notifications/task-detail-modal";
|
||||||
import { TaskNotificationsTrigger } from "@/components/notifications/task-notifications-trigger";
|
import { TaskNotificationsTrigger } from "@/components/notifications/task-notifications-trigger";
|
||||||
|
import { useSidebarPreference } from "@/components/providers/sidebar-preference-provider";
|
||||||
import {
|
import {
|
||||||
companyAnalysisQueryOptions,
|
companyAnalysisQueryOptions,
|
||||||
companyFinancialStatementsQueryOptions,
|
companyFinancialStatementsQueryOptions,
|
||||||
@@ -37,6 +38,7 @@ import { buildGraphingHref } from "@/lib/graphing/catalog";
|
|||||||
import type { ActiveContext, NavGroup, NavItem } from "@/lib/types";
|
import type { ActiveContext, NavGroup, NavItem } from "@/lib/types";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import { useTaskNotificationsCenter } from "@/hooks/use-task-notifications-center";
|
import { useTaskNotificationsCenter } from "@/hooks/use-task-notifications-center";
|
||||||
|
import { SIDEBAR_PREFERENCE_KEY } from "@/lib/sidebar-preference";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
type AppShellProps = {
|
type AppShellProps = {
|
||||||
@@ -148,8 +150,6 @@ const GROUP_LABELS: Record<NavGroup, string> = {
|
|||||||
portfolio: "Portfolio",
|
portfolio: "Portfolio",
|
||||||
};
|
};
|
||||||
|
|
||||||
const SIDEBAR_PREFERENCE_KEY = "fiscal-shell-sidebar-collapsed";
|
|
||||||
|
|
||||||
function normalizeTicker(value: string | null | undefined) {
|
function normalizeTicker(value: string | null | undefined) {
|
||||||
const normalized = value?.trim().toUpperCase() ?? "";
|
const normalized = value?.trim().toUpperCase() ?? "";
|
||||||
return normalized.length > 0 ? normalized : null;
|
return normalized.length > 0 ? normalized : null;
|
||||||
@@ -260,12 +260,16 @@ export function AppShell({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const searchParams = useSearchParams();
|
const searchParams = useSearchParams();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
|
const { initialSidebarCollapsed } = useSidebarPreference();
|
||||||
|
|
||||||
const [isSigningOut, setIsSigningOut] = useState(false);
|
const [isSigningOut, setIsSigningOut] = useState(false);
|
||||||
const [isMoreOpen, setIsMoreOpen] = useState(false);
|
const [isMoreOpen, setIsMoreOpen] = useState(false);
|
||||||
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(false);
|
const [isSidebarCollapsed, setIsSidebarCollapsed] = useState(
|
||||||
const [hasLoadedSidebarPreference, setHasLoadedSidebarPreference] =
|
initialSidebarCollapsed,
|
||||||
|
);
|
||||||
|
const [hasResolvedSidebarPreference, setHasResolvedSidebarPreference] =
|
||||||
useState(false);
|
useState(false);
|
||||||
|
const [hasMounted, setHasMounted] = useState(false);
|
||||||
const notifications = useTaskNotificationsCenter();
|
const notifications = useTaskNotificationsCenter();
|
||||||
const { data: session } = authClient.useSession();
|
const { data: session } = authClient.useSession();
|
||||||
const sessionUser = (session?.user ?? null) as {
|
const sessionUser = (session?.user ?? null) as {
|
||||||
@@ -433,14 +437,14 @@ export function AppShell({
|
|||||||
const storedPreference = window.localStorage.getItem(
|
const storedPreference = window.localStorage.getItem(
|
||||||
SIDEBAR_PREFERENCE_KEY,
|
SIDEBAR_PREFERENCE_KEY,
|
||||||
);
|
);
|
||||||
if (storedPreference === "true") {
|
if (storedPreference === "true" || storedPreference === "false") {
|
||||||
setIsSidebarCollapsed(true);
|
setIsSidebarCollapsed(storedPreference === "true");
|
||||||
}
|
}
|
||||||
setHasLoadedSidebarPreference(true);
|
setHasResolvedSidebarPreference(true);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!hasLoadedSidebarPreference) {
|
if (!hasResolvedSidebarPreference) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -448,7 +452,12 @@ export function AppShell({
|
|||||||
SIDEBAR_PREFERENCE_KEY,
|
SIDEBAR_PREFERENCE_KEY,
|
||||||
String(isSidebarCollapsed),
|
String(isSidebarCollapsed),
|
||||||
);
|
);
|
||||||
}, [hasLoadedSidebarPreference, isSidebarCollapsed]);
|
document.cookie = `${SIDEBAR_PREFERENCE_KEY}=${String(isSidebarCollapsed)}; path=/; max-age=31536000; samesite=lax`;
|
||||||
|
}, [hasResolvedSidebarPreference, isSidebarCollapsed]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
setHasMounted(true);
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const browserWindow = window as Window & {
|
const browserWindow = window as Window & {
|
||||||
@@ -520,7 +529,8 @@ export function AppShell({
|
|||||||
>
|
>
|
||||||
<aside
|
<aside
|
||||||
className={cn(
|
className={cn(
|
||||||
"hidden shrink-0 flex-col gap-4 border-r border-[color:var(--line-weak)] transition-[width,padding] duration-200 lg:flex",
|
"hidden shrink-0 flex-col gap-4 border-r border-[color:var(--line-weak)] lg:flex",
|
||||||
|
hasMounted ? "transition-[width,padding] duration-200" : "",
|
||||||
isSidebarCollapsed ? "w-16 pr-1" : "w-72 pr-4",
|
isSidebarCollapsed ? "w-16 pr-1" : "w-72 pr-4",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
|||||||
7
lib/sidebar-preference.ts
Normal file
7
lib/sidebar-preference.ts
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
export const SIDEBAR_PREFERENCE_KEY = "fiscal-shell-sidebar-collapsed";
|
||||||
|
|
||||||
|
export function parseSidebarPreference(
|
||||||
|
value: string | null | undefined,
|
||||||
|
): boolean {
|
||||||
|
return value === "true";
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user