PatternsArchitecture Patterns
Mediator Pattern
Components communicate with a Mediator instead of directly with each other, avoiding "spaghetti code"
The Mediator Pattern defines an object that encapsulates how a set of objects interact. Instead of components communicating directly with each other (mess!), they communicate with a "Mediator" (like a central Hook or State Manager), avoiding "spaghetti code".
🎯 The Problem
// ❌ BAD: Components communicating directly
function Header({ onSearch }) {
return <input onChange={(e) => onSearch(e.target.value)} />;
}
function Sidebar({ searchTerm, onFilter }) {
return <Filter onChange={onFilter} />;
}
function Main({ searchTerm, filter, onSort }) {
// Too many props being passed! 😱
return <List items={filtered} onSort={onSort} />;
}
function App() {
const [searchTerm, setSearchTerm] = useState('');
const [filter, setFilter] = useState('');
const [sort, setSort] = useState('');
// Props drilling! 😱
return (
<>
<Header onSearch={setSearchTerm} />
<Sidebar searchTerm={searchTerm} onFilter={setFilter} />
<Main searchTerm={searchTerm} filter={filter} onSort={setSort} />
</>
);
}
// ✅ GOOD: Mediator Pattern
class AppMediator {
private searchTerm = '';
private filter = '';
private sort = '';
private listeners: Array<() => void> = [];
setSearchTerm(term: string) {
this.searchTerm = term;
this.notify();
}
setFilter(filter: string) {
this.filter = filter;
this.notify();
}
getState() {
return { searchTerm: this.searchTerm, filter: this.filter, sort: this.sort };
}
subscribe(listener: () => void) {
this.listeners.push(listener);
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) this.listeners.splice(index, 1);
};
}
private notify() {
this.listeners.forEach(listener => listener());
}
}
// Components communicate only with mediator
function Header({ mediator }) {
return <input onChange={(e) => mediator.setSearchTerm(e.target.value)} />;
}📚 Common Front-End Examples
1. Mediator for Complex Forms
class FormMediator {
private fields: Record<string, unknown> = {};
private errors: Record<string, string> = {};
private listeners: Array<() => void> = [];
setField(name: string, value: unknown) {
this.fields[name] = value;
this.validateField(name);
this.notify();
}
getField(name: string) {
return this.fields[name];
}
getErrors() {
return this.errors;
}
validateField(name: string) {
const value = this.fields[name];
if (name === 'email' && !value?.includes('@')) {
this.errors[name] = 'Invalid email';
} else {
delete this.errors[name];
}
}
subscribe(listener: () => void) {
this.listeners.push(listener);
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) this.listeners.splice(index, 1);
};
}
private notify() {
this.listeners.forEach(listener => listener());
}
}
// Components don't communicate with each other, only with mediator
function EmailField({ mediator }: { mediator: FormMediator }) {
const value = mediator.getField('email');
const errors = mediator.getErrors();
return (
<div>
<input
value={value || ''}
onChange={(e) => mediator.setField('email', e.target.value)}
/>
{errors.email && <span>{errors.email}</span>}
</div>
);
}
function SubmitButton({ mediator }: { mediator: FormMediator }) {
const errors = mediator.getErrors();
const isValid = Object.keys(errors).length === 0;
return (
<button disabled={!isValid}>
Submit
</button>
);
}2. Mediator for Chat/Messages
class ChatMediator {
private messages: Message[] = [];
private users: User[] = [];
private listeners: Array<() => void> = [];
sendMessage(userId: string, text: string) {
const message: Message = {
id: Math.random().toString(),
userId,
text,
timestamp: Date.now()
};
this.messages.push(message);
this.notify();
}
getMessages() {
return [...this.messages];
}
addUser(user: User) {
this.users.push(user);
this.notify();
}
subscribe(listener: () => void) {
this.listeners.push(listener);
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) this.listeners.splice(index, 1);
};
}
private notify() {
this.listeners.forEach(listener => listener());
}
}
// Components don't know other components
function MessageInput({ mediator, userId }: { mediator: ChatMediator; userId: string }) {
const [text, setText] = useState('');
const handleSend = () => {
mediator.sendMessage(userId, text);
setText('');
};
return (
<div>
<input value={text} onChange={(e) => setText(e.target.value)} />
<button onClick={handleSend}>Send</button>
</div>
);
}
function MessageList({ mediator }: { mediator: ChatMediator }) {
const [messages, setMessages] = useState(mediator.getMessages());
useEffect(() => {
const unsubscribe = mediator.subscribe(() => {
setMessages(mediator.getMessages());
});
return unsubscribe;
}, [mediator]);
return (
<div>
{messages.map(msg => (
<div key={msg.id}>{msg.text}</div>
))}
</div>
);
}3. Mediator for Drag and Drop
class DragDropMediator {
private draggedItem: unknown = null;
private dropZones: Map<string, { onDrop: (item: unknown) => void }> = new Map();
private listeners: Array<() => void> = [];
startDrag(item: unknown) {
this.draggedItem = item;
this.notify();
}
registerDropZone(id: string, zone: { onDrop: (item: unknown) => void }) {
this.dropZones.set(id, zone);
}
drop(zoneId: string) {
const zone = this.dropZones.get(zoneId);
if (zone && this.draggedItem) {
zone.onDrop(this.draggedItem);
this.draggedItem = null;
this.notify();
}
}
getDraggedItem() {
return this.draggedItem;
}
subscribe(listener: () => void) {
this.listeners.push(listener);
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) this.listeners.splice(index, 1);
};
}
private notify() {
this.listeners.forEach(listener => listener());
}
}
function DraggableItem<T>({ item, mediator }: { item: T; mediator: DragDropMediator }) {
const handleDragStart = () => {
mediator.startDrag(item);
};
return (
<div draggable onDragStart={handleDragStart}>
{item.name}
</div>
);
}
function DropZone<T>({ id, mediator, onDrop }: { id: string; mediator: DragDropMediator; onDrop: (item: T) => void }) {
useEffect(() => {
mediator.registerDropZone(id, { onDrop });
}, [id, mediator, onDrop]);
const handleDrop = () => {
mediator.drop(id);
};
return (
<div onDrop={handleDrop} onDragOver={(e) => e.preventDefault()}>
Drop here
</div>
);
}4. Mediator for Modal/Dialog System
class ModalMediator {
private modals: Map<string, { component: React.ComponentType<Record<string, unknown>>; props: Record<string, unknown> }> = new Map();
private activeModal: string | null = null;
private listeners: Array<() => void> = [];
open<P extends Record<string, unknown>>(id: string, component: React.ComponentType<P>, props: P) {
this.modals.set(id, { component, props });
this.activeModal = id;
this.notify();
}
close(id: string) {
this.modals.delete(id);
if (this.activeModal === id) {
this.activeModal = null;
}
this.notify();
}
getActiveModal() {
if (!this.activeModal) return null;
return this.modals.get(this.activeModal);
}
subscribe(listener: () => void) {
this.listeners.push(listener);
return () => {
const index = this.listeners.indexOf(listener);
if (index > -1) this.listeners.splice(index, 1);
};
}
private notify() {
this.listeners.forEach(listener => listener());
}
}
// Components open modals through mediator
function DeleteButton({ mediator, itemId }: { mediator: ModalMediator; itemId: string }) {
const handleClick = () => {
mediator.open('confirm-delete', ConfirmDialog, {
onConfirm: () => deleteItem(itemId),
onCancel: () => mediator.close('confirm-delete')
});
};
return <button onClick={handleClick}>Delete</button>;
}
function ModalRenderer({ mediator }: { mediator: ModalMediator }) {
const [activeModal, setActiveModal] = useState(mediator.getActiveModal());
useEffect(() => {
const unsubscribe = mediator.subscribe(() => {
setActiveModal(mediator.getActiveModal());
});
return unsubscribe;
}, [mediator]);
if (!activeModal) return null;
const { component: Component, props } = activeModal;
return <Component {...props} />;
}5. Mediator as React Hook
function useMediator<T>() {
const [state, setState] = useState<T>({} as T);
const listenersRef = useRef<Array<(state: T) => void>>([]);
const set = useCallback((updates: Partial<T>) => {
setState(prev => {
const next = { ...prev, ...updates };
listenersRef.current.forEach(listener => listener(next));
return next;
});
}, []);
const subscribe = useCallback((listener: (state: T) => void) => {
listenersRef.current.push(listener);
return () => {
const index = listenersRef.current.indexOf(listener);
if (index > -1) listenersRef.current.splice(index, 1);
};
}, []);
return { state, set, subscribe };
}
// Usage
function App() {
const mediator = useMediator<{ search: string; filter: string }>();
return (
<>
<Header mediator={mediator} />
<Sidebar mediator={mediator} />
<Main mediator={mediator} />
</>
);
}
function Header({ mediator }: { mediator: ReturnType<typeof useMediator<{ search: string; filter: string }>> }) {
return (
<input
placeholder="Search..."
onChange={(e) => mediator.set({ search: e.target.value })}
/>
);
}
function Sidebar({ mediator }: { mediator: ReturnType<typeof useMediator<{ search: string; filter: string }>> }) {
const { state } = mediator;
return (
<div>
<input
placeholder="Filter..."
value={state.filter || ''}
onChange={(e) => mediator.set({ filter: e.target.value })}
/>
</div>
);
}
function Main({ mediator }: { mediator: ReturnType<typeof useMediator<{ search: string; filter: string }>> }) {
const { state } = mediator;
return (
<div>
<p>Search: {state.search || 'none'}</p>
<p>Filter: {state.filter || 'none'}</p>
</div>
);
}
## 🎯 When to Use
This pattern is commonly used and recommended for:
1. **Complex forms** - Multiple fields that depend on each other
2. **Chat systems** - Multiple components need to coordinate (input, list, notifications)
3. **Drag and drop** - Coordinate between draggable items and drop zones
4. **Modal systems** - Centralize opening/closing of modals
5. **Dashboards** - Multiple widgets that need to share state
## 📚 Key Takeaways
- **Reduces coupling** - Components don't know other components
- **Centralizes communication** - All flow goes through mediator
- **Facilitates maintenance** - Changes in one component don't affect others
- **Avoids props drilling** - Don't need to pass props through multiple levels
- **Testability** - Easy to mock mediator in tests