Front-end Engineering Lab
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

On this page