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