Skip to Content
SymfonyPartie 12 — Temps réel & asynchrone12.6 — Consommer Mercure dans Next.js

Chapitre 12.6 — Consommer Mercure dans Next.js

Partie 12 : Temps réel & async — Chapitre 6/7 Version de référence : Symfony 7.4 / Mercure / Next.js 15.

⏱️ TL;DR

  • Côté client, on s’abonne avec l’API native EventSource : on ouvre une connexion vers l’URL publique du hub avec le topic en query, et on écoute les messages.
  • C’est un Client Component ('use client') : SSE est une API navigateur ; on l’ouvre dans un useEffect et on ferme au démontage.
  • Pour des topics privés, le JWT subscriber est envoyé via un cookie (posé côté serveur) ; le hub l’exige.
  • Pattern typique : données initiales chargées par le Server Component (RSC, Partie 11.8), puis mises à jour en direct via Mercure côté client. Le meilleur des deux mondes.
  • Gérer la reconnexion (EventSource la fait en partie) et le nettoyage.
  • Pour un dev Next.js : EventSource ≈ ce que vous feriez avec Pusher/Ably, mais sans SDK — API web standard.

🎯 Objectifs

  • S’abonner à un topic Mercure depuis Next.js.
  • Combiner données initiales (RSC) et mises à jour live.
  • Gérer topics privés, reconnexion et nettoyage.

1. Le pattern : RSC pour l’initial, Mercure pour le live

  • Le Server Component rend l’état initial (ex. le nombre de places, via l’API — Partie 11.8), pour un affichage immédiat et référençable (SEO).
  • Un Client Component s’abonne ensuite au topic Mercure et met à jour l’affichage en temps réel.

2. Un hook d’abonnement

// app/components/useMercure.ts 'use client'; import { useEffect, useState } from 'react'; export function useMercureTopic<T>(topic: string, initial: T): T { const [data, setData] = useState<T>(initial); useEffect(() => { const url = new URL(process.env.NEXT_PUBLIC_MERCURE_URL!); // URL publique du hub url.searchParams.append('topic', topic); const es = new EventSource(url, { withCredentials: true }); // cookie JWT pour topics privés es.onmessage = (event) => { setData(JSON.parse(event.data)); }; es.onerror = () => { /* EventSource retente automatiquement */ }; return () => es.close(); // NETTOYAGE au démontage (crucial) }, [topic]); return data; }

Utilisation dans un composant qui affiche les places restantes d’un créneau :

'use client'; import { useMercureTopic } from './useMercure'; export function SeatsLive({ slotId, initialSeats }: { slotId: number; initialSeats: number }) { const data = useMercureTopic( `https://booklab.test/slots/${slotId}`, { seatsLeft: initialSeats }, ); return <span>{data.seatsLeft} places restantes</span>; }

Quand Symfony publie une mise à jour (chapitre 12.5), tous les navigateurs affichant ce créneau voient le nombre changer en direct, sans rechargement ni polling.

⚠️ Piège nettoyage : toujours fermer l’EventSource dans le retour du useEffect (es.close()). Sinon, en navigation SPA (App Router), les connexions s’accumulent (fuite de connexions/mémoire, messages dupliqués). C’est le même réflexe que le cleanup useEffect habituel — mais ici, oublié, il ouvre des dizaines de flux SSE.

3. Topics privés : le JWT subscriber

Pour un topic privé (notifications d’un utilisateur), le hub exige un JWT subscriber. On le fournit via un cookie (mercureAuthorization) posé côté serveur (une Route Handler Next.js, après login, comme pour le JWT d’API — Partie 11.8). EventSource avec withCredentials: true envoie ce cookie ; le hub vérifie que le JWT autorise le topic.

4. Configuration côté hub

Le hub doit autoriser l’origine du front (CORS pour SSE) et accepter les cookies. En dev, l’URL publique du hub (NEXT_PUBLIC_MERCURE_URL) pointe vers le hub local. En prod, si l’on passe par le proxy Next.js (Partie 11.6), on peut router /.well-known/mercure via Next pour rester en même origine (et éviter le CORS SSE).

💡 Pour un dev Next.js : EventSource est natif (pas de dépendance), reconnexion automatique incluse. Comparé à Pusher/Ably, vous contrôlez le hub (auto-hébergé) et Symfony pilote la publication. Le pattern RSC (initial) + EventSource (live) est idéal en App Router : rendu serveur rapide + temps réel client, sans sur-ingénierie. Pour de l’ultra-haute fréquence (jeu, dessin collaboratif), on regarderait WebSocket ; pour de la diffusion d’updates (places, notifs, listes), SSE/Mercure est parfait.

✏️ Exercices

Exercice 1. Écrivez un composant client qui affiche en direct le nombre de places d’un créneau, en partant d’une valeur initiale rendue par le serveur.

✅ Solution

Voir le §2 : un hook useMercureTopic(topic, initial) ouvre un EventSource sur .../slots/{id}, met à jour l’état à chaque message, et ferme la connexion au démontage. Le composant SeatsLive reçoit initialSeats (rendu par le Server Component) et affiche data.seatsLeft. L’état initial vient du RSC (Partie 11.8), les mises à jour de Mercure.

Exercice 2. Pourquoi le nettoyage de l’EventSource est-il critique en App Router ?

✅ Solution

Parce que la navigation SPA (App Router) monte/démonte les composants sans rechargement complet : sans es.close() dans le cleanup du useEffect, chaque montage ouvre une nouvelle connexion SSE sans fermer les précédentes → accumulation de connexions (fuite), messages dupliqués, charge inutile sur le hub. Le cleanup ferme proprement le flux à chaque démontage.

Exercice 3. Comment un topic privé est-il autorisé côté client ?

✅ Solution

Par un JWT subscriber transmis au hub via un cookie (posé côté serveur Next.js après login, comme le JWT d’API). EventSource(url, { withCredentials: true }) envoie ce cookie ; le hub vérifie que le JWT autorise le topic demandé et ne délivre le flux que si c’est le cas. Le client ne manipule jamais le JWT en clair.

🧠 Quiz de révision

1. Avec quelle API navigateur s’abonne-t-on à Mercure ?

L’API native EventSource (SSE), en passant le topic en query vers l’URL publique du hub.

2. Dans quel type de composant ouvre-t-on l’EventSource ?

Un Client Component ('use client'), dans un useEffect, avec fermeture (es.close()) au démontage.

3. Quel pattern combine SSR et temps réel ?

RSC pour l’état initial (rendu serveur, SEO) + EventSource pour les mises à jour live côté client.

4. Comment recevoir un topic privé ?

Via un JWT subscriber en cookie (posé côté serveur) + EventSource(..., { withCredentials: true }) ; le hub vérifie l’autorisation.

5. Pourquoi fermer l’EventSource au démontage ?

Pour éviter l’accumulation de connexions SSE (fuite, messages dupliqués) lors des navigations SPA.


Prochain chapitre : 12.7 — Stripe : paiement & webhooks — encaisser une réservation en toute sécurité.