Observer
Notifier automatiquement des objets des changements d'un autre objet
Problème
Un objet change d'état et d'autres objets doivent en être informés, sans que l'objet source connaisse ses observateurs à l'avance.
Implémentation de base
type Listener<T> = (value: T) => void;
class EventEmitter<T> {
private listeners = new Set<Listener<T>>();
subscribe(listener: Listener<T>): () => void {
this.listeners.add(listener);
// Retourne une fonction de désabonnement
return () => this.listeners.delete(listener);
}
emit(value: T): void {
this.listeners.forEach(listener => listener(value));
}
}
// Usage
const cartUpdated = new EventEmitter<Cart>();
const unsubscribe = cartUpdated.subscribe((cart) => {
console.log('Cart updated:', cart.items.length, 'items');
});
cartUpdated.emit(newCart); // notifie tous les abonnés
// Nettoyage
unsubscribe();Store réactif avec Observer
type Observer<T> = (state: T) => void;
class ReactiveStore<T> {
private observers = new Set<Observer<T>>();
constructor(private state: T) {}
getState(): T {
return this.state;
}
setState(updater: Partial<T> | ((prev: T) => Partial<T>)): void {
const patch = typeof updater === 'function' ? updater(this.state) : updater;
this.state = { ...this.state, ...patch };
this.observers.forEach(obs => obs(this.state));
}
subscribe(observer: Observer<T>): () => void {
this.observers.add(observer);
return () => this.observers.delete(observer);
}
}
// C'est le principe de Zustand, Redux, et tous les state managers
const cartStore = new ReactiveStore({
items: [] as CartItem[],
total: 0,
});
cartStore.subscribe((state) => {
document.title = `(${state.items.length}) Mon panier`;
});Observer en React : useEffect
useEffect implémente le pattern Observer : le composant s'abonne aux changements de ses dépendances.
function CartBadge() {
const items = useCartStore((state) => state.items);
// useCartStore utilise subscribe de Zustand en interne
// Le composant est un Observer du store
return <span>{items.length}</span>;
}Observer avec RxJS (flux asynchrones)
Pour des cas complexes (debounce, combinaison de sources), RxJS formalise le pattern Observer :
import { fromEvent, debounceTime, distinctUntilChanged, switchMap } from 'rxjs';
import { from } from 'rxjs';
const searchInput = document.getElementById('search') as HTMLInputElement;
const subscription = fromEvent(searchInput, 'input')
.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap((event) => {
const term = (event.target as HTMLInputElement).value;
return from(searchProducts(term));
}),
)
.subscribe((results) => {
setSearchResults(results);
});
// Cleanup
subscription.unsubscribe();En React : pattern pub/sub cross-composants
// EventBus singleton pour communication entre composants non liés
class EventBus {
private emitters = new Map<string, EventEmitter<unknown>>();
private getEmitter<T>(event: string): EventEmitter<T> {
if (!this.emitters.has(event)) {
this.emitters.set(event, new EventEmitter<unknown>());
}
return this.emitters.get(event) as EventEmitter<T>;
}
on<T>(event: string, listener: Listener<T>): () => void {
return this.getEmitter<T>(event).subscribe(listener);
}
emit<T>(event: string, value: T): void {
this.getEmitter<T>(event).emit(value);
}
}
export const eventBus = new EventBus();
// Composant A émet
function AddToCartButton({ product }: { product: Product }) {
const handleClick = () => {
eventBus.emit('cart:item-added', { product, quantity: 1 });
};
return <button onClick={handleClick}>Ajouter</button>;
}
// Composant B écoute (avec cleanup)
function CartToast() {
const [message, setMessage] = useState('');
useEffect(() => {
const unsubscribe = eventBus.on<{ product: Product }>('cart:item-added', ({ product }) => {
setMessage(`${product.name} ajouté au panier`);
setTimeout(() => setMessage(''), 3000);
});
return unsubscribe; // cleanup au unmount
}, []);
if (!message) return null;
return <div className="toast">{message}</div>;
}Le retour de useEffect est exactement la fonction de désabonnement, c'est la mécanique centrale du pattern Observer en React.
Exercice : Observer dans le Pokémon Builder
Contexte
Actuellement, quand l'équipe est complète et qu'on clique sur "Ajouter", rien ne se passe visuellement, le return silencieux dans handleAdd ignore l'action sans aucun feedback :
function handleAdd(pokemon: Pokemon) {
if (team.length >= 6) return; // ← silencieux
// ...
}Pour ajouter une notification, il faudrait que App.tsx gère lui-même l'UI de la notification, ce qui mélange deux responsabilités.
Objectif
Implémenter un bus d'événements (Observer) pour déclencher des notifications découplées du composant principal.
Instructions
-
Crée le fichier
src/observer/teamEvents.ts. -
Implémente une classe
EventBusgénérique avec les méthodes :on(event: string, callback): () => void, abonne un observateur, retourne une fonction de désabonnementemit(event: string, payload?), notifie tous les abonnés
-
Exporte une instance partagée
teamEvents. -
Crée un composant
src/components/Notification.tsxqui s'abonne à l'événement"team:full"viauseEffectet affiche un message temporaire. -
Dans
App.tsx, émetsteamEvents.emit("team:full")quand on tente d'ajouter un Pokémon alors que l'équipe est pleine. -
Ajoute
<Notification />dans le JSX deApp.tsx.
Résultat attendu
App.tsx ne sait pas qu'une notification existe. Notification ne sait pas comment l'équipe fonctionne. Les deux communiquent uniquement via les événements.