From 29127d7ed51e1f8f9fd91ed4e12f59a17bb756e0 Mon Sep 17 00:00:00 2001 From: Usman Baig Date: Wed, 18 Mar 2026 18:32:50 +0100 Subject: [PATCH] fix: eliminate all loading flashes in sidebar site picker MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Root cause 1: hydration mismatch — SSR rendered collapsed=true but client useState initializer read localStorage synchronously, causing an immediate state change and visual flash. Fix: always initialize collapsed=true, read localStorage in useEffect so the transition is smooth (collapsed→expanded animates cleanly). Root cause 2: three-phase badge rendering (skeleton→letter→favicon) caused visible state changes. Fix: just show the empty orange badge until the favicon arrives. No skeleton, no letter fallback. One state transition: empty→favicon. --- components/dashboard/Sidebar.tsx | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/components/dashboard/Sidebar.tsx b/components/dashboard/Sidebar.tsx index f40f2bf..4d7fcf2 100644 --- a/components/dashboard/Sidebar.tsx +++ b/components/dashboard/Sidebar.tsx @@ -86,7 +86,6 @@ function SitePicker({ sites, siteId, collapsed, onExpand, onCollapse, wasCollaps const pathname = usePathname() const router = useRouter() const currentSite = sites.find((s) => s.id === siteId) - const initial = currentSite?.name?.charAt(0)?.toUpperCase() || '?' const faviconUrl = currentSite?.domain ? `${FAVICON_SERVICE_URL}?domain=${currentSite.domain}&sz=64` : null useEffect(() => { @@ -129,23 +128,19 @@ function SitePicker({ sites, siteId, collapsed, onExpand, onCollapse, wasCollaps }} className="w-full flex items-center gap-2.5 rounded-lg px-2.5 py-2 text-sm font-medium text-neutral-700 dark:text-neutral-200 hover:bg-neutral-100 dark:hover:bg-neutral-800 overflow-hidden" > - - {faviconUrl && !faviconFailed ? ( + + {faviconUrl && !faviconFailed && ( {currentSite?.name setFaviconFailed(true)} /> - ) : currentSite ? ( - initial - ) : ( - )} @@ -245,10 +240,13 @@ export default function Sidebar({ const [sites, setSites] = useState([]) const [pendingHref, setPendingHref] = useState(null) const wasCollapsedRef = useRef(false) - const [collapsed, setCollapsed] = useState(() => { - if (typeof window === 'undefined') return true // SSR: default collapsed to avoid hydration flash - return localStorage.getItem(SIDEBAR_KEY) !== 'false' // default collapsed unless explicitly set to false - }) + const [collapsed, setCollapsed] = useState(true) // Always start collapsed — no hydration mismatch + + // Read saved state after mount — expands smoothly if user had it open + useEffect(() => { + const saved = localStorage.getItem(SIDEBAR_KEY) + if (saved === 'false') setCollapsed(false) + }, []) useEffect(() => { listSites().then(setSites).catch(() => {}) }, []) useEffect(() => { setPendingHref(null); onMobileClose() }, [pathname, onMobileClose])