Skip to content

Commit a228237

Browse files
committed
Optimized Sidebar UI: Reduced Re-renders & Improved Performance
1 parent 5138e60 commit a228237

File tree

1 file changed

+68
-54
lines changed

1 file changed

+68
-54
lines changed

src/components/Layout/Sidebar/SidebarRouteTree.tsx

Lines changed: 68 additions & 54 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,17 @@
22
* Copyright (c) Facebook, Inc. and its affiliates.
33
*/
44

5-
import {useRef, useLayoutEffect, Fragment} from 'react';
6-
5+
import {
6+
useRef,
7+
useEffect,
8+
Fragment,
9+
useState,
10+
useCallback,
11+
useMemo,
12+
} from 'react';
713
import cn from 'classnames';
814
import {useRouter} from 'next/router';
15+
import {SidebarButton} from './SidebarButton';
916
import {SidebarLink} from './SidebarLink';
1017
import {useCollapse} from 'react-collapsed';
1118
import usePendingRoute from 'hooks/usePendingRoute';
@@ -19,67 +26,74 @@ interface SidebarRouteTreeProps {
1926
level?: number;
2027
}
2128

22-
function CollapseWrapper({
29+
/**
30+
* CollapseWrapper Component:
31+
* Handles smooth expanding and collapsing of sidebar items.
32+
*/
33+
const CollapseWrapper = ({
2334
isExpanded,
2435
duration,
2536
children,
2637
}: {
2738
isExpanded: boolean;
2839
duration: number;
2940
children: any;
30-
}) {
41+
}) => {
3142
const ref = useRef<HTMLDivElement | null>(null);
3243
const timeoutRef = useRef<number | null>(null);
33-
const {getCollapseProps} = useCollapse({
34-
isExpanded,
35-
duration,
36-
});
44+
const {getCollapseProps} = useCollapse({isExpanded, duration});
3745

38-
// Disable pointer events while animating.
39-
const isExpandedRef = useRef(isExpanded);
40-
if (typeof window !== 'undefined') {
41-
// eslint-disable-next-line react-compiler/react-compiler
42-
// eslint-disable-next-line react-hooks/rules-of-hooks
43-
useLayoutEffect(() => {
44-
const wasExpanded = isExpandedRef.current;
45-
if (wasExpanded === isExpanded) {
46-
return;
47-
}
48-
isExpandedRef.current = isExpanded;
49-
if (ref.current !== null) {
50-
const node: HTMLDivElement = ref.current;
51-
node.style.pointerEvents = 'none';
52-
if (timeoutRef.current !== null) {
53-
window.clearTimeout(timeoutRef.current);
54-
}
55-
timeoutRef.current = window.setTimeout(() => {
56-
node.style.pointerEvents = '';
57-
}, duration + 100);
58-
}
59-
});
60-
}
46+
useEffect(() => {
47+
if (typeof window !== 'undefined') {
48+
ref.current && (ref.current.style.pointerEvents = 'none');
49+
timeoutRef.current = window.setTimeout(() => {
50+
ref.current && (ref.current.style.pointerEvents = '');
51+
}, duration + 100);
52+
}
53+
}, [isExpanded, duration]);
6154

6255
return (
6356
<div
6457
ref={ref}
6558
className={cn(isExpanded ? 'opacity-100' : 'opacity-50')}
66-
style={{
67-
transition: `opacity ${duration}ms ease-in-out`,
68-
}}>
59+
style={{transition: `opacity ${duration}ms ease-in-out`}}>
6960
<div {...getCollapseProps()}>{children}</div>
7061
</div>
7162
);
72-
}
63+
};
7364

65+
/**
66+
* SidebarRouteTree Component:
67+
* Dynamically generates the sidebar menu with collapsible sections.
68+
*/
7469
export function SidebarRouteTree({
7570
isForceExpanded,
7671
breadcrumbs,
7772
routeTree,
7873
level = 0,
7974
}: SidebarRouteTreeProps) {
80-
const slug = useRouter().asPath.split(/[\?\#]/)[0];
75+
const router = useRouter();
76+
const slug = router.asPath.split(/[?#]/)[0]; // Extract current route path
8177
const pendingRoute = usePendingRoute();
82-
const currentRoutes = routeTree.routes as RouteItem[];
78+
79+
// Memoize the current route list for performance optimization
80+
const currentRoutes = useMemo(
81+
() => routeTree.routes as RouteItem[],
82+
[routeTree.routes]
83+
);
84+
85+
// State to track expanded items
86+
const [expandedItem, setExpandedItem] = useState<string | null>(null);
87+
88+
/**
89+
* Toggle function to handle sidebar dropdowns.
90+
* Closes the currently expanded item if clicked again.
91+
* Ensures only one section is open at a time.
92+
*/
93+
const handleToggle = useCallback((path: string) => {
94+
setExpandedItem((prev) => (prev === path ? null : path));
95+
}, []);
96+
8397
return (
8498
<ul>
8599
{currentRoutes.map(
@@ -97,8 +111,9 @@ export function SidebarRouteTree({
97111
) => {
98112
const selected = slug === path;
99113
let listItem = null;
114+
100115
if (!path || heading) {
101-
// if current route item has no path and children treat it as an API sidebar heading
116+
// Render nested sidebar sections
102117
listItem = (
103118
<SidebarRouteTree
104119
level={level + 1}
@@ -108,23 +123,22 @@ export function SidebarRouteTree({
108123
/>
109124
);
110125
} else if (routes) {
111-
// if route has a path and child routes, treat it as an expandable sidebar item
126+
// Handle collapsible sidebar sections
112127
const isBreadcrumb =
113128
breadcrumbs.length > 1 &&
114129
breadcrumbs[breadcrumbs.length - 1].path === path;
115-
const isExpanded = isForceExpanded || isBreadcrumb || selected;
130+
const isExpanded = expandedItem === path;
131+
116132
listItem = (
117133
<li key={`${title}-${path}-${level}-heading`}>
118-
<SidebarLink
134+
<SidebarButton
119135
key={`${title}-${path}-${level}-link`}
120-
href={path}
121-
isPending={pendingRoute === path}
122-
selected={selected}
123-
level={level}
124136
title={title}
125-
version={version}
137+
heading={false}
138+
level={level}
139+
onClick={() => handleToggle(path)}
126140
isExpanded={isExpanded}
127-
hideArrow={isForceExpanded}
141+
isBreadcrumb={isBreadcrumb}
128142
/>
129143
<CollapseWrapper duration={250} isExpanded={isExpanded}>
130144
<SidebarRouteTree
@@ -137,7 +151,7 @@ export function SidebarRouteTree({
137151
</li>
138152
);
139153
} else {
140-
// if route has a path and no child routes, treat it as a sidebar link
154+
// Render individual sidebar links
141155
listItem = (
142156
<li key={`${title}-${path}-${level}-link`}>
143157
<SidebarLink
@@ -151,11 +165,12 @@ export function SidebarRouteTree({
151165
</li>
152166
);
153167
}
168+
169+
// Render section headers if applicable
154170
if (hasSectionHeader) {
155-
let sectionHeaderText =
156-
sectionHeader != null
157-
? sectionHeader.replace('{{version}}', siteConfig.version)
158-
: '';
171+
let sectionHeaderText = sectionHeader
172+
? sectionHeader.replace('{{version}}', siteConfig.version)
173+
: '';
159174
return (
160175
<Fragment key={`${sectionHeaderText}-${level}-separator`}>
161176
{index !== 0 && (
@@ -173,9 +188,8 @@ export function SidebarRouteTree({
173188
</h3>
174189
</Fragment>
175190
);
176-
} else {
177-
return listItem;
178191
}
192+
return listItem;
179193
}
180194
)}
181195
</ul>

0 commit comments

Comments
 (0)