Skip to main content

Sidebar Manager Pattern

Pattern d'architecture pour gérer les sidebars dans une application React avec un gestionnaire centralisé.

Problème

Dans une application complexe, gérer plusieurs sidebars peut devenir chaotique :

  • État dispersé dans différents composants
  • Logique de fermeture/ouverture dupliquée
  • Difficile de maintenir une seule sidebar ouverte à la fois

Solution

Un Sidebar Manager centralisé avec des providers dédiés pour chaque sidebar.

Architecture

src/
├── features/
│ └── sidebar/
│ ├── SidebarManager.tsx
│ ├── SidebarProvider.tsx
│ ├── hooks/
│ │ └── useSidebar.ts
│ └── sidebars/
│ ├── CommentSidebar/
│ │ ├── CommentSidebarProvider.tsx
│ │ ├── useCommentSidebar.ts
│ │ └── CommentSidebar.tsx
│ └── SettingsSidebar/
│ ├── SettingsSidebarProvider.tsx
│ ├── useSettingsSidebar.ts
│ └── SettingsSidebar.tsx

Implémentation

1. SidebarManager

Le gestionnaire central qui track quelle sidebar est ouverte :

import { createContext, useContext, useState, ReactNode } from 'react';

type SidebarType = 'comments' | 'settings' | null;

interface SidebarManagerContextValue {
activeSidebar: SidebarType;
openSidebar: (sidebar: SidebarType) => void;
closeSidebar: () => void;
}

const SidebarManagerContext = createContext<SidebarManagerContextValue | undefined>(undefined);

export function SidebarManagerProvider({ children }: { children: ReactNode }) {
const [activeSidebar, setActiveSidebar] = useState<SidebarType>(null);

const openSidebar = (sidebar: SidebarType) => {
setActiveSidebar(sidebar);
};

const closeSidebar = () => {
setActiveSidebar(null);
};

return (
<SidebarManagerContext.Provider value={{ activeSidebar, openSidebar, closeSidebar }}>
{children}
</SidebarManagerContext.Provider>
);
}

export function useSidebarManager() {
const context = useContext(SidebarManagerContext);
if (!context) {
throw new Error('useSidebarManager must be used within SidebarManagerProvider');
}
return context;
}

2. Provider spécifique (exemple: Comments)

import { createContext, useContext, ReactNode } from 'react';
import { useSidebarManager } from '../SidebarManager';

interface CommentSidebarContextValue {
isOpen: boolean;
open: () => void;
close: () => void;
// Logique métier spécifique
selectedCommentId: string | null;
selectComment: (id: string) => void;
}

const CommentSidebarContext = createContext<CommentSidebarContextValue | undefined>(undefined);

export function CommentSidebarProvider({ children }: { children: ReactNode }) {
const { activeSidebar, openSidebar, closeSidebar } = useSidebarManager();
const [selectedCommentId, setSelectedCommentId] = useState<string | null>(null);

const isOpen = activeSidebar === 'comments';

const open = () => {
openSidebar('comments');
};

const close = () => {
closeSidebar();
setSelectedCommentId(null); // Cleanup
};

const selectComment = (id: string) => {
setSelectedCommentId(id);
open();
};

return (
<CommentSidebarContext.Provider
value={{ isOpen, open, close, selectedCommentId, selectComment }}
>
{children}
</CommentSidebarContext.Provider>
);
}

export function useCommentSidebar() {
const context = useContext(CommentSidebarContext);
if (!context) {
throw new Error('useCommentSidebar must be used within CommentSidebarProvider');
}
return context;
}

3. Composant Sidebar

import { useCommentSidebar } from './CommentSidebarProvider';

export function CommentSidebar() {
const { isOpen, close, selectedCommentId } = useCommentSidebar();

if (!isOpen) return null;

return (
<div className="sidebar">
<button onClick={close}>Close</button>
<div>
{selectedCommentId ? (
<CommentDetail id={selectedCommentId} />
) : (
<CommentList />
)}
</div>
</div>
);
}

4. Utilisation dans l'app

function App() {
return (
<SidebarManagerProvider>
<CommentSidebarProvider>
<SettingsSidebarProvider>
<MainContent />
<CommentSidebar />
<SettingsSidebar />
</SettingsSidebarProvider>
</CommentSidebarProvider>
</SidebarManagerProvider>
);
}

function MainContent() {
const { selectComment } = useCommentSidebar();
const { open: openSettings } = useSettingsSidebar();

return (
<div>
<button onClick={() => selectComment('comment-123')}>
View Comment
</button>
<button onClick={openSettings}>
Settings
</button>
</div>
);
}

Avantages

Un seul point de vérité : Le manager sait quelle sidebar est ouverte
Isolation : Chaque sidebar a sa propre logique métier
Fermeture automatique : Ouvrir une sidebar ferme automatiquement l'autre
Type-safe : TypeScript garantit l'utilisation correcte
Testable : Chaque provider est testable indépendamment

Extensions possibles

  • Ajouter des animations de transition
  • Supporter plusieurs sidebars ouvertes simultanément (avec un array)
  • Historique de navigation entre sidebars
  • Persister l'état dans localStorage

Ressources