Skip to main content

Sidebar Manager Pattern

Architecture pattern for managing sidebars in a React application with a centralized manager.

🎯 Problem​

In a complex application, managing multiple sidebars can become chaotic:

  • State scattered across different components
  • Duplicated open/close logic
  • Hard to ensure only one sidebar is open at a time

📂 Architecture​

src/
├── features/
│ └── sidebar/
│ ├── SidebarManager.tsx
│ ├── SidebarRenderer.tsx
│ ├── SidebarWrapper.tsx
│ ├── types.ts
│ ├── hooks/
│ │ └── useRegisterSidebar.ts
│ └── sidebars/
│ ├── comments/
│ │ ├── useCommentSidebar.ts
│ │ └── CommentsList.tsx
│ ├── MetadataSidebar/
│ │ ├── useMetadataSidebar.ts
│ │ └── Metadata.tsx

📜 Implementation​

1. SidebarManager​

First, define a few types in types.ts:

// Extend this union type with more sidebar identifiers as needed
export type SidebarId = 'comments' | 'metadata';

export type SidebarConfig<TProps = Record<string, any>> = {
component: React.ComponentType<TProps>;
id: SidebarId;
label: string;
};

The central manager SidebarManager.tsx that tracks which sidebar is open:

import { createContext, useCallback, useContext, useMemo, useState } from 'react';

import type { SidebarConfig, SidebarId } from './types';

type CloseSidebar = () => void;
type RegisterSidebar = <TProps = Record<string, any>>(config: SidebarConfig<TProps>) => void;
type ToggleSidebar = (id: SidebarId, options?: { force?: boolean }) => void;
type UnregisterSidebar = (id: SidebarId) => void;

type SidebarContextType = {
activeSidebarId: SidebarId | null;
sidebarProps: Map<SidebarId, Record<string, any>>;
sidebars: Map<SidebarId, SidebarConfig<any>>;
closeSidebar: CloseSidebar;
registerSidebar: RegisterSidebar;
toggleSidebar: ToggleSidebar;
unregisterSidebar: UnregisterSidebar;
updateSidebarProps: (id: SidebarId, props: Record<string, any>) => void;
};

const SidebarManagerContext = createContext<SidebarContextType | null>(null);

export function useSidebarManager() {
const context = useContext(SidebarManagerContext);

if (!context) {
throw new Error('useSidebarManager must be used within SidebarManagerProvider');
}

return context;
}

type SidebarManagerProviderProps =
| { children: React.ReactNode }
| { children: (props: SidebarContextType) => React.ReactNode };

export const SidebarManagerProvider = ({ children }: SidebarManagerProviderProps) => {
const [activeSidebarId, setActiveSidebarId] = useState<SidebarId | null>(null);
const [sidebarProps, setSidebarProps] = useState<Map<SidebarId, Record<string, any>>>(new Map());
const [sidebars, setSidebars] = useState(new Map<SidebarId, SidebarConfig<any>>());

const closeSidebar = useCallback<CloseSidebar>(() => setActiveSidebarId(null), []);

const registerSidebar = useCallback<RegisterSidebar>((config) => {
setSidebars((prev) => new Map(prev).set(config.id, config));
}, []);

const toggleSidebar = useCallback<ToggleSidebar>((id, options) => {
if (options?.force === true) {
setActiveSidebarId(id);
} else {
setActiveSidebarId((prev) => (prev === id ? null : id));
}
}, []);

const unregisterSidebar = useCallback<UnregisterSidebar>((id) => {
setSidebars((prev) => {
const next = new Map(prev);
next.delete(id);
return next;
});
}, []);

const updateSidebarProps = useCallback((id: SidebarId, props: Record<string, any>) => {
setSidebarProps((prev) => new Map(prev).set(id, props));
}, []);

const value = useMemo(() => {
return {
activeSidebarId,
sidebarProps,
sidebars,
closeSidebar,
registerSidebar,
toggleSidebar,
unregisterSidebar,
updateSidebarProps,
};
}, [
activeSidebarId,
sidebarProps,
sidebars,
closeSidebar,
registerSidebar,
toggleSidebar,
unregisterSidebar,
updateSidebarProps,
]);

return (
<SidebarManagerContext.Provider value={value}>
{typeof children === 'function' ? children(value) : children}
</SidebarManagerContext.Provider>
);
};

The SidebarRenderer.tsx will handle rendering:

import { useSidebarManager } from './SidebarManager';

export function SidebarRenderer() {
const { sidebars, activeSidebarId, sidebarProps } = useSidebarManager();

if (!activeSidebarId) return null;

const activeSidebar = sidebars.get(activeSidebarId);
if (!activeSidebar) return null;

const Component = activeSidebar.component;
const props = sidebarProps.get(activeSidebarId) || {};

return <Component {...props} />;
}

And eventually define a hook hooks/useRegisterSidebar.ts to simplify the usage:

import { useEffect } from 'react';

import { useSidebarManager } from '../SidebarManager';
import type { SidebarConfig } from '../types';

export function useRegisterSidebar<TProps = Record<string, any>>(config: SidebarConfig<TProps>) {
const { registerSidebar, unregisterSidebar } = useSidebarManager();

useEffect(() => {
registerSidebar(config);
return () => unregisterSidebar(config.id);
}, [config.id, registerSidebar, unregisterSidebar]);
}

2. Sidebar Wrapper Component​

Eventually define a wrapper for rendering purpose:

import type { PropsWithChildren, ReactNode } from 'react';

import { Button, Flex, Typography } from '../../../../ui';
import { useSidebarManager } from './SidebarManager';

interface SidebarWrapperProps extends Omit<HTMLDivElement, 'title'> {
title: ReactNode;
}

export const SidebarWrapper = ({ title, children }: PropsWithChildren<SidebarWrapperProps>) => {
const { closeSidebar } = useSidebarManager();

const titleComponent = () => {
if (typeof title === 'string') {
return <Typography> {title}</Typography>;
}

return title;
};

return (
<div>
<Flex justify="space-between">
{titleComponent()}
<Button label={$t({ id: 'button.close', defaultMessage: 'Close' })} onClick={closeSidebar} />
</Flex>
{children}
</div>
);
};

3. Declare the sidebar in use<SIDEBAR_ID>Sidebar​

For example, declare your comment sidebar in sidebars/comments/useCommentsSidebar.tsx

import { useEffect } from 'react';

import { useRegisterSidebar } from '../../hooks/useRegisterSidebar';
import { useSidebarManager } from '../../SidebarManager';
import { SidebarWrapper } from '../../SidebarWrapper';

interface SidebarProps {
prop1: any;
prop2: any;
}

export function useCommentSidebar(prop1: any, prop2: any) {
const { updateSidebarProps } = useSidebarManager();

useRegisterSidebar<SidebarProps>({
id: 'comments',
component: Sidebar,
label: 'Comments',
});

useEffect(() => {
updateSidebarProps('comments', { prop1, prop2 });
}, [prop1, prop2, updateSidebarProps]);
}

const Sidebar = ({ prop1, prop2 }: SidebarProps) => {
return (
<SidebarWrapper title="Comments">
{/* Render your sidebar content here using prop1 and prop2 */}
</SidebarWrapper>
)
};


4. Usage in the App​

function App() {
return (
<SidebarManagerProvider>
{({ activeSidebarId }) => (
{/* use activeSidebarId if needed */}
{/* otherwise, it will be accessible within the <MainContent /> and its children */}
<MainContent />
)}
</SidebarManagerProvider>
);
}

function MainContent() {
const { activeSidebarId, toggleSidebar } = useSidebarManager();

return (
<Page>
<Drawer open={!!activeSidebarId} drawerContent={<SidebarRenderer />}>
{/* your page content here */}
</Drawer.History>
</Page>
);
}

✅ Benefits​

  • Single source of truth: The manager knows which sidebar is open
  • Isolation: Each sidebar has its own business logic
  • Automatic closing: Opening a sidebar automatically closes the other
  • Type-safe: TypeScript ensures correct usage
  • Testable: Each provider can be tested independently

🧩 Possible Extensions​

  • Add transition animations
  • Support multiple sidebars open simultaneously (using an array)
  • Navigation history between sidebars
  • Persist state in localStorage