Skip to content

Optimized Sidebar UI: Reduced Re-renders & Improved Performance Fixes #7721 #7725

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 68 additions & 54 deletions src/components/Layout/Sidebar/SidebarRouteTree.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,17 @@
* Copyright (c) Facebook, Inc. and its affiliates.
*/

import {useRef, useLayoutEffect, Fragment} from 'react';

import {
useRef,
useEffect,
Fragment,
useState,
useCallback,
useMemo,
} from 'react';
import cn from 'classnames';
import {useRouter} from 'next/router';
import {SidebarButton} from './SidebarButton';
import {SidebarLink} from './SidebarLink';
import {useCollapse} from 'react-collapsed';
import usePendingRoute from 'hooks/usePendingRoute';
Expand All @@ -19,67 +26,74 @@ interface SidebarRouteTreeProps {
level?: number;
}

function CollapseWrapper({
/**
* CollapseWrapper Component:
* Handles smooth expanding and collapsing of sidebar items.
*/
const CollapseWrapper = ({
isExpanded,
duration,
children,
}: {
isExpanded: boolean;
duration: number;
children: any;
}) {
}) => {
const ref = useRef<HTMLDivElement | null>(null);
const timeoutRef = useRef<number | null>(null);
const {getCollapseProps} = useCollapse({
isExpanded,
duration,
});
const {getCollapseProps} = useCollapse({isExpanded, duration});

// Disable pointer events while animating.
const isExpandedRef = useRef(isExpanded);
if (typeof window !== 'undefined') {
// eslint-disable-next-line react-compiler/react-compiler
// eslint-disable-next-line react-hooks/rules-of-hooks
useLayoutEffect(() => {
const wasExpanded = isExpandedRef.current;
if (wasExpanded === isExpanded) {
return;
}
isExpandedRef.current = isExpanded;
if (ref.current !== null) {
const node: HTMLDivElement = ref.current;
node.style.pointerEvents = 'none';
if (timeoutRef.current !== null) {
window.clearTimeout(timeoutRef.current);
}
timeoutRef.current = window.setTimeout(() => {
node.style.pointerEvents = '';
}, duration + 100);
}
});
}
useEffect(() => {
if (typeof window !== 'undefined') {
ref.current && (ref.current.style.pointerEvents = 'none');
timeoutRef.current = window.setTimeout(() => {
ref.current && (ref.current.style.pointerEvents = '');
}, duration + 100);
}
}, [isExpanded, duration]);

return (
<div
ref={ref}
className={cn(isExpanded ? 'opacity-100' : 'opacity-50')}
style={{
transition: `opacity ${duration}ms ease-in-out`,
}}>
style={{transition: `opacity ${duration}ms ease-in-out`}}>
<div {...getCollapseProps()}>{children}</div>
</div>
);
}
};

/**
* SidebarRouteTree Component:
* Dynamically generates the sidebar menu with collapsible sections.
*/
export function SidebarRouteTree({
isForceExpanded,
breadcrumbs,
routeTree,
level = 0,
}: SidebarRouteTreeProps) {
const slug = useRouter().asPath.split(/[\?\#]/)[0];
const router = useRouter();
const slug = router.asPath.split(/[?#]/)[0]; // Extract current route path
const pendingRoute = usePendingRoute();
const currentRoutes = routeTree.routes as RouteItem[];

// Memoize the current route list for performance optimization
const currentRoutes = useMemo(
() => routeTree.routes as RouteItem[],
[routeTree.routes]
);

// State to track expanded items
const [expandedItem, setExpandedItem] = useState<string | null>(null);

/**
* Toggle function to handle sidebar dropdowns.
* Closes the currently expanded item if clicked again.
* Ensures only one section is open at a time.
*/
const handleToggle = useCallback((path: string) => {
setExpandedItem((prev) => (prev === path ? null : path));
}, []);

return (
<ul>
{currentRoutes.map(
Expand All @@ -97,8 +111,9 @@ export function SidebarRouteTree({
) => {
const selected = slug === path;
let listItem = null;

if (!path || heading) {
// if current route item has no path and children treat it as an API sidebar heading
// Render nested sidebar sections
listItem = (
<SidebarRouteTree
level={level + 1}
Expand All @@ -108,23 +123,22 @@ export function SidebarRouteTree({
/>
);
} else if (routes) {
// if route has a path and child routes, treat it as an expandable sidebar item
// Handle collapsible sidebar sections
const isBreadcrumb =
breadcrumbs.length > 1 &&
breadcrumbs[breadcrumbs.length - 1].path === path;
const isExpanded = isForceExpanded || isBreadcrumb || selected;
const isExpanded = expandedItem === path;

listItem = (
<li key={`${title}-${path}-${level}-heading`}>
<SidebarLink
<SidebarButton
key={`${title}-${path}-${level}-link`}
href={path}
isPending={pendingRoute === path}
selected={selected}
level={level}
title={title}
version={version}
heading={false}
level={level}
onClick={() => handleToggle(path)}
isExpanded={isExpanded}
hideArrow={isForceExpanded}
isBreadcrumb={isBreadcrumb}
/>
<CollapseWrapper duration={250} isExpanded={isExpanded}>
<SidebarRouteTree
Expand All @@ -137,7 +151,7 @@ export function SidebarRouteTree({
</li>
);
} else {
// if route has a path and no child routes, treat it as a sidebar link
// Render individual sidebar links
listItem = (
<li key={`${title}-${path}-${level}-link`}>
<SidebarLink
Expand All @@ -151,11 +165,12 @@ export function SidebarRouteTree({
</li>
);
}

// Render section headers if applicable
if (hasSectionHeader) {
let sectionHeaderText =
sectionHeader != null
? sectionHeader.replace('{{version}}', siteConfig.version)
: '';
let sectionHeaderText = sectionHeader
? sectionHeader.replace('{{version}}', siteConfig.version)
: '';
return (
<Fragment key={`${sectionHeaderText}-${level}-separator`}>
{index !== 0 && (
Expand All @@ -173,9 +188,8 @@ export function SidebarRouteTree({
</h3>
</Fragment>
);
} else {
return listItem;
}
return listItem;
}
)}
</ul>
Expand Down