fix: load Sidebar with ssr:false — zero server-rendered content
The sidebar now uses next/dynamic with ssr:false, meaning it renders NOTHING in the server HTML. No DOM content = no possible flash of "Ci" or any text during SSR-to-hydration gap. The sidebar only mounts on the client where localStorage is immediately available, so collapsed state is correct from the very first render.
This commit is contained in:
@@ -1,9 +1,12 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import { useState, useCallback } from 'react'
|
import { useState, useCallback } from 'react'
|
||||||
import Sidebar from './Sidebar'
|
import dynamic from 'next/dynamic'
|
||||||
import ContentHeader from './ContentHeader'
|
import ContentHeader from './ContentHeader'
|
||||||
|
|
||||||
|
// Load sidebar only on the client — prevents any SSR flash of text/labels
|
||||||
|
const Sidebar = dynamic(() => import('./Sidebar'), { ssr: false })
|
||||||
|
|
||||||
export default function DashboardShell({
|
export default function DashboardShell({
|
||||||
siteId,
|
siteId,
|
||||||
children,
|
children,
|
||||||
|
|||||||
@@ -240,16 +240,10 @@ export default function Sidebar({
|
|||||||
const [sites, setSites] = useState<Site[]>([])
|
const [sites, setSites] = useState<Site[]>([])
|
||||||
const [pendingHref, setPendingHref] = useState<string | null>(null)
|
const [pendingHref, setPendingHref] = useState<string | null>(null)
|
||||||
const wasCollapsedRef = useRef(false)
|
const wasCollapsedRef = useRef(false)
|
||||||
const [ready, setReady] = useState(false)
|
// Safe to read localStorage directly — this component is loaded with ssr:false
|
||||||
const [collapsed, setCollapsed] = useState(true)
|
const [collapsed, setCollapsed] = useState(() => {
|
||||||
|
return localStorage.getItem(SIDEBAR_KEY) !== 'false'
|
||||||
// Read saved state and reveal sidebar in one frame — no flash
|
})
|
||||||
useEffect(() => {
|
|
||||||
const saved = localStorage.getItem(SIDEBAR_KEY)
|
|
||||||
if (saved === 'false') setCollapsed(false)
|
|
||||||
// Reveal after state is set — React batches these, so sidebar appears at correct width
|
|
||||||
requestAnimationFrame(() => setReady(true))
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => { listSites().then(setSites).catch(() => {}) }, [])
|
useEffect(() => { listSites().then(setSites).catch(() => {}) }, [])
|
||||||
useEffect(() => { setPendingHref(null); onMobileClose() }, [pathname, onMobileClose])
|
useEffect(() => { setPendingHref(null); onMobileClose() }, [pathname, onMobileClose])
|
||||||
@@ -340,17 +334,13 @@ export default function Sidebar({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Desktop — empty shell until ready, then real content */}
|
{/* Desktop — ssr:false means this only renders on client, no hydration flash */}
|
||||||
{!ready ? (
|
<aside
|
||||||
<div className="hidden md:block shrink-0 border-r border-neutral-200/60 dark:border-neutral-800/60 bg-white/90 dark:bg-neutral-900/90 backdrop-blur-xl" style={{ width: COLLAPSED }} />
|
className="hidden md:flex flex-col shrink-0 border-r border-neutral-200/60 dark:border-neutral-800/60 bg-white/90 dark:bg-neutral-900/90 backdrop-blur-xl overflow-hidden"
|
||||||
) : (
|
style={{ width: collapsed ? COLLAPSED : EXPANDED, transition: 'width 200ms cubic-bezier(0.4, 0, 0.2, 1)' }}
|
||||||
<aside
|
>
|
||||||
className="hidden md:flex flex-col shrink-0 border-r border-neutral-200/60 dark:border-neutral-800/60 bg-white/90 dark:bg-neutral-900/90 backdrop-blur-xl overflow-hidden"
|
{sidebarContent(false)}
|
||||||
style={{ width: collapsed ? COLLAPSED : EXPANDED, transition: 'width 200ms cubic-bezier(0.4, 0, 0.2, 1)' }}
|
</aside>
|
||||||
>
|
|
||||||
{sidebarContent(false)}
|
|
||||||
</aside>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* Mobile overlay */}
|
{/* Mobile overlay */}
|
||||||
{mobileOpen && (
|
{mobileOpen && (
|
||||||
|
|||||||
Reference in New Issue
Block a user