Skip to content

Commit e3eac29

Browse files
Refactor Docs layout to remove Suspense and improve loading performance; update DocSidebar to use useLayoutEffect for better link activation handling and prevent flickering during route transitions.
1 parent f0e371a commit e3eac29

3 files changed

Lines changed: 38 additions & 42 deletions

File tree

app/docs/[[...slug]]/layout.tsx

Lines changed: 1 addition & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,5 @@
1-
import { Suspense } from 'react';
21
import { getAuthenticatedClerkUserId } from '@/lib/users';
32
import { DocsLayoutWithNav } from './DocsLayoutWithNav';
4-
import { DocsRouteLoading } from './DocsRouteLoading';
53
import { redirect } from 'next/navigation';
64

75
export default async function DocsLayout({
@@ -14,9 +12,5 @@ export default async function DocsLayout({
1412
redirect('/sign-in');
1513
}
1614

17-
return (
18-
<Suspense fallback={<DocsRouteLoading />}>
19-
<DocsLayoutWithNav>{children}</DocsLayoutWithNav>
20-
</Suspense>
21-
);
15+
return <DocsLayoutWithNav>{children}</DocsLayoutWithNav>;
2216
}

components/docs/DocSidebar.tsx

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
"use client"
22

3-
import React, { useState, useEffect, memo, useRef, useCallback } from 'react';
3+
import React, { useState, useEffect, useLayoutEffect, memo, useRef, useCallback } from 'react';
44
import Link from 'next/link';
55
import { usePathname, useRouter } from 'next/navigation';
66
import { cn } from '@/lib/utils';
@@ -18,37 +18,49 @@ const SidebarActiveSync = ({
1818
currentPathProp?: string;
1919
}) => {
2020
const pathname = usePathname();
21+
const prevActiveLinksRef = useRef<Set<Element>>(new Set());
2122

22-
useEffect(() => {
23+
useLayoutEffect(() => {
2324
const navElement = navRef.current;
2425
if (!navElement) return;
2526

2627
const currentPathValue = currentPathProp ?? pathname;
2728
pathnameRef.current = currentPathValue;
2829

29-
// Update active states via DOM manipulation (no React state updates)
30-
navElement.querySelectorAll('[data-nav-href]').forEach((link) => {
30+
// Build next active set first, then only mutate changed links to avoid blink.
31+
const allLinks = Array.from(navElement.querySelectorAll('[data-nav-href]'));
32+
const nextActiveLinks = new Set<Element>();
33+
34+
allLinks.forEach((link) => {
35+
const href = link.getAttribute('data-nav-href');
36+
if (!href) return;
37+
if (href === currentPathValue) {
38+
nextActiveLinks.add(link);
39+
return;
40+
}
41+
if (href === '/docs' && currentPathValue !== '/docs') return;
42+
if (currentPathValue.startsWith(href + '/')) {
43+
nextActiveLinks.add(link);
44+
}
45+
});
46+
47+
const prevActiveLinks = prevActiveLinksRef.current;
48+
49+
// Deactivate only links that are no longer active.
50+
prevActiveLinks.forEach((link) => {
51+
if (nextActiveLinks.has(link)) return;
3152
link.classList.remove('bg-blue-50', 'text-blue-600', 'font-medium');
3253
link.classList.add('text-gray-700', 'hover:bg-gray-100', 'hover:text-gray-900');
3354
});
3455

35-
// Add active class to current active items (exact match)
36-
const activeLinks = navElement.querySelectorAll(`[data-nav-href="${currentPathValue}"]`);
37-
activeLinks.forEach((link) => {
56+
// Activate only newly active links.
57+
nextActiveLinks.forEach((link) => {
58+
if (prevActiveLinks.has(link)) return;
3859
link.classList.add('bg-blue-50', 'text-blue-600', 'font-medium');
3960
link.classList.remove('text-gray-700', 'hover:bg-gray-100', 'hover:text-gray-900');
4061
});
4162

42-
// Also handle sub-path matching
43-
navElement.querySelectorAll('[data-nav-href]').forEach((link) => {
44-
const href = link.getAttribute('data-nav-href');
45-
if (!href || href === currentPathValue) return;
46-
if (href === '/docs' && currentPathValue !== '/docs') return;
47-
if (currentPathValue.startsWith(href + '/')) {
48-
link.classList.add('bg-blue-50', 'text-blue-600', 'font-medium');
49-
link.classList.remove('text-gray-700', 'hover:bg-gray-100', 'hover:text-gray-900');
50-
}
51-
});
63+
prevActiveLinksRef.current = nextActiveLinks;
5264
}, [pathname, currentPathProp, navRef, pathnameRef]);
5365

5466
return null;
@@ -134,9 +146,9 @@ const DocSidebarComponent: React.FC<DocSidebarProps> = ({
134146
router.prefetch(href);
135147
}, [router]);
136148

137-
// After mount: restore expanded state from localStorage and ensure current route is visible.
138-
// This will cause at most ONE re-render after hydration (acceptable) and prevents hydration mismatch.
139-
useEffect(() => {
149+
// Restore expanded state before paint to avoid visible collapse->expand flicker
150+
// when this component remounts during route transitions.
151+
useLayoutEffect(() => {
140152
if (typeof window === 'undefined') return;
141153

142154
const setsEqual = (a: Set<string>, b: Set<string>) => {
@@ -303,10 +315,13 @@ const DocSidebarComponent: React.FC<DocSidebarProps> = ({
303315
const isProject = pathSegments.length === 3 && pathSegments[0] === 'docs' && pathSegments[1] === 'projects';
304316
// Documents under projects have 4+ segments: /docs/projects/{projectId}/{docId}
305317
const isProjectDocument = pathSegments.length >= 4 && pathSegments[0] === 'docs' && pathSegments[1] === 'projects';
318+
const itemKey = isCollapsibleHeader
319+
? `header-${level}-${item.label}`
320+
: `${item.href}-${level}`;
306321

307322
return (
308323
<div
309-
key={item.label}
324+
key={itemKey}
310325
className={cn(
311326
level > 0 && level === 1 && !isProject && 'ml-1',
312327
level > 0 && level === 1 && isProject && 'ml-4',

components/docs/DocsLayoutClient.tsx

Lines changed: 1 addition & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,14 @@
11
"use client";
22

33
import React, { useMemo, useRef, useCallback, useState } from 'react';
4-
import dynamic from 'next/dynamic';
54
import { NavItem } from './DocSidebar';
65
import { Header } from '@/components/sections/Header';
76
import type { ProcessedProject, ProcessedYourDoc } from '@/lib/docs';
87
import { useCreateProject } from './CreateProjectHandler';
98
import { useCreateDoc } from './CreateDocHandler';
109
import { useRenameDelete } from './useRenameDelete';
1110
import { NavigationProvider, DocsContentArea } from './NavigationContext';
12-
13-
const StableSidebar = dynamic(
14-
() => import('./StableSidebar').then((mod) => mod.StableSidebar),
15-
{
16-
ssr: false,
17-
loading: () => (
18-
<aside
19-
className="fixed left-0 top-16 z-30 hidden h-[calc(100vh-4rem)] w-64 border-r border-gray-100 bg-white md:block"
20-
aria-hidden
21-
/>
22-
),
23-
}
24-
);
11+
import { StableSidebar } from './StableSidebar';
2512

2613
interface DocsLayoutClientProps {
2714
sidebarItems: NavItem[];

0 commit comments

Comments
 (0)