React · WebSocket · Temps Réel

WebSocket & Pair Programming

Notifications temps réel · Reconnexion · File d'attente offline

Utilisez les flèches, cliquez ou glissez pour naviguer

Objectifs de la leçon

1. Comprendre WebSocket vs HTTP

Protocole persistant, bidirectionnel, temps réel

2. Connexion WebSocket + reconnexion

Exponential backoff, heartbeat ping/pong

3. Système de notifications

Toasts éphémères + badges persistants via Zustand

4. Afficher le partenaire de pair programming

Assignation quotidienne depuis le WebSocket

5. Gérer le mode offline

File d'attente de messages, reprise à la reconnexion

Plan du cours

1

Pair Programming dans Bootcode

Assignation quotidienne, affichage du partenaire via WS

2

WebSocket natif du navigateur

Connexion, envoi de messages, fermeture propre

3

Stratégies de reconnexion

Exponential backoff, heartbeat ping/pong

4

Architecture notification : WS → store → UI

Toast/badge/liste via Zustand dédié

5

File d'attente offline

Stocker les events quand l'utilisateur est déconnecté

Le Pair Programming

dans l'application Bootcode

👥

Assignation quotidienne

Un partenaire différent chaque jour

Mis à jour en temps réel

Affiché via WebSocket dès la connexion

🔔

Notifications

Toast + badge non-lu

Afficher le partenaire du jour

Composant React — PairPartner.tsx

function PairPartner() {

const { partner } = usePairStore();

if (!partner) return <PartnerSkeleton />;

return (

<div className="flex items-center gap-3 p-4 bg-cyan-50 rounded-lg">

<Avatar src={partner.avatar} />

<div>

<p className="font-semibold">{partner.name}</p>

<p className="text-sm text-cyan-600">Partenaire du jour</p>

</div>

</div>

);

}

💡 Le composant lit depuis un store Zustand — le WS écrit dans le store, pas directement dans le composant

HTTP vs WebSocket

HTTP

🔄 Requête → Réponse → Connexion fermée

📤 Client initie toujours

⏱️ Polling : re-demander toutes les X secondes

💸 Overhead élevé (headers HTTP à chaque fois)

// Polling toutes les 2s (mauvais)

setInterval(() => fetch('/api/partner'), 2000)

WebSocket

🔗 Connexion persistante bidirectionnelle

📨 Serveur peut pousser à tout moment

⚡ Latence très faible

✅ Overhead minimal après handshake initial

// Une seule connexion

new WebSocket('wss://api.bootcode.dev/ws')

L'API WebSocket native du navigateur

// 1. Créer la connexion

const ws = new WebSocket('wss://api.bootcode.dev/ws');

// 2. Connexion ouverte

ws.onopen = () => console.log('Connecté !');

// 3. Recevoir un message

ws.onmessage = (event) => {

const data = JSON.parse(event.data);

console.log('Reçu :', data);

};

// 4. Envoyer un message

ws.send(JSON.stringify({ type: 'ping' }));

// 5. Connexion fermée

ws.onclose = () => console.log('Déconnecté');

onopen

Connecté

onmessage

Message reçu

send()

Envoyer

onclose

Déconnecté

WebSocket dans un useEffect

useEffect(() => {

// Connexion au montage du composant

const ws = new WebSocket('wss://api.bootcode.dev/ws');

ws.onmessage = (event) => {

const msg = JSON.parse(event.data);

handleMessage(msg);

};

// ⚠️ Cleanup OBLIGATOIRE

return () => ws.close();

}, []);

⚠️ Sans le return () => ws.close()

La connexion reste ouverte même si le composant est démonté → memory leak + connexions fantômes

Stratégies de Reconnexion

Les connexions se coupent — toujours

📶

Réseau instable

WiFi, mobile, tunnel...

😴

Mise en veille

Le PC dort, la connexion meurt

🔄

Redémarrage serveur

Déploiement, maintenance

Exponential Backoff

Attendre de plus en plus longtemps entre chaque tentative

Évite de surcharger le serveur avec des reconnexions en rafale

1s

Tentative 1

2s

Tentative 2

4s

Tentative 3

8s

Tentative 4

30s

Max

const reconnect = (attempt = 0) => {

const delay = Math.min(1000 * Math.pow(2, attempt), 30000);

setTimeout(() => connect(attempt + 1), delay);

};

Heartbeat : ping / pong

Détecter les connexions "zombies" — ouvertes mais silencieuses

// Envoyer un ping toutes les 30 secondes

const heartbeat = setInterval(() => {

if (ws.readyState === WebSocket.OPEN) {

ws.send(JSON.stringify({ type: 'ping' }));

}

}, 30000);

// Écouter le pong du serveur

ws.onmessage = (e) => {

const { type } = JSON.parse(e.data);

if (type === 'pong') lastPong = Date.now();

};

💡 Si aucun pong reçu depuis 60s → connexion morte → forcer la reconnexion

Hook useWebSocket

function useWebSocket(url: string) {

const [status, setStatus] = useState('connecting');

const wsRef = useRef(null);

const attemptRef = useRef(0);

const connect = useCallback(() => {

const ws = new WebSocket(url);

wsRef.current = ws;

ws.onopen = () => { setStatus('connected'); attemptRef.current = 0; };

ws.onclose = () => { setStatus('reconnecting'); scheduleReconnect(); };

}, [url]);

useEffect(() => { connect(); return () => wsRef.current?.close(); }, []);

return { status, ws: wsRef.current };

}

Architecture Notifications

WS → Store → UI

WebSocket

Reçoit les events

🗃️

Zustand Store

État centralisé

🎨

Composants UI

Toast · Badge · Liste

Les composants ne parlent jamais directement au WebSocket

Store Zustand — Notifications

interface NotifStore {

notifications: Notification[];

unreadCount: number;

addNotif: (n: Notification) => void;

markAllRead: () => void;

}

const useNotifStore = create<NotifStore>((set) => ({

notifications: [],

unreadCount: 0,

addNotif: (n) => set((s) => ({

notifications: [n, ...s.notifications],

unreadCount: s.unreadCount + 1,

})),

markAllRead: () => set({ unreadCount: 0 }),

}));

Toasts éphémères vs Badges persistants

Toast

⏱️ Disparaît après 5 secondes

📍 Position fixe (coin bas-droite)

🔔 Pour les events importants

// Disparaît automatiquement

setTimeout(() => removeToast(id), 5000);

Badge

♾️ Persistant jusqu'au clic

🔢 Affiche le nombre non-lu

📬 Sur l'icône cloche

// Réinitialisé au clic

onClick={() => markAllRead()}

// Composant badge notification

const { unreadCount } = useNotifStore();

return <span className="badge">{unreadCount}</span>

Composant ToastContainer

function ToastContainer() {

const { toasts, removeToast } = useNotifStore();

return (

<div className="fixed bottom-4 right-4 flex flex-col gap-2">

{toasts.map((toast) => (

<Toast

key={toast.id}

message={toast.message}

onClose={() => removeToast(toast.id)}

/>

))}

</div>

);

}

💡 Ce composant est placé une seule fois dans App.tsx — il est toujours présent dans le DOM

Mode Offline

L'app doit fonctionner sans WebSocket

📦

File d'attente

Stocker les events reçus hors-ligne pour les rejouer à la reconnexion

🚦

Indicateur de statut

Afficher clairement : connecté / reconnexion en cours / offline

File d'attente de messages

const messageQueue: string[] = [];

// Envoyer ou mettre en file si déconnecté

function sendMessage(msg: object) {

const payload = JSON.stringify(msg);

if (wsRef.current?.readyState === WebSocket.OPEN) {

wsRef.current.send(payload);

} else {

messageQueue.push(payload); // en attente

}

}

// À la reconnexion, vider la file

ws.onopen = () => {

while (messageQueue.length > 0) {

ws.send(messageQueue.shift()!);

}

};

Indicateur de statut de connexion

connected

WebSocket actif et opérationnel

reconnecting

Tentative de reconnexion en cours

offline

Hors ligne, messages en file

const statusColors = {

connected: 'bg-green-500',

reconnecting: 'bg-amber-500 animate-spin',

offline: 'bg-red-500',

};

⚠️ Pièges courants

À éviter absolument

Memory Leak

Pas de cleanup → connexions fantômes

DDoS involontaire

Reconnexion sans backoff → rafale de requêtes

DOM direct pour toasts

Manipuler le DOM au lieu d'un store

App cassée sans WS

Ne pas gérer le mode offline

Piège 1 : Memory Leak

❌ L'erreur

useEffect(() => {

const ws = new WebSocket(url);

// ...

// Pas de return!

}, []);

Le composant est démonté mais le WS reste ouvert. Reconnexion multiple → fuites mémoire

✅ La solution

useEffect(() => {

const ws = new WebSocket(url);

// ...

return () => ws.close();

}, []);

Le cleanup ferme proprement la connexion au démontage

Piège 2 : DDoS involontaire

❌ Reconnexion immédiate

ws.onclose = () => {

connect(); // Immédiat!

};

Si le serveur est down → des milliers de requêtes par seconde → le serveur ne peut pas redémarrer

✅ Exponential backoff

ws.onclose = () => {

const d = Math.min(

1000 * 2 ** attempt, 30000

);

setTimeout(connect, d);

};

Piège 3 : Manipuler le DOM pour les toasts

❌ DOM direct

ws.onmessage = (e) => {

const el = document.createElement('div');

el.textContent = e.data;

document.body.appendChild(el);

};

Hors du cycle React → impossible à tester, à styliser, à animer proprement

✅ Via le store Zustand

ws.onmessage = (e) => {

const msg = JSON.parse(e.data);

addNotif({ message: msg.text });

};

Le store déclenche un re-render React → le ToastContainer gère l'affichage

Points clés à retenir

⚡ Un seul WebSocket au démarrage de l'app

Pas dans chaque composant — dans un hook partagé ou un store

🔄 Toujours implémenter la reconnexion avec backoff

Les connexions se coupent — 1s, 2s, 4s, 8s… max 30s

🗃️ Les notifications passent par un store Zustand dédié

Jamais directement dans un composant ou dans le DOM

💓 Heartbeat pour détecter les connexions mortes

ping toutes les 30s, reconnexion si aucun pong depuis 60s

📦 File d'attente en mode offline

L'app doit fonctionner sans WS, rejouer les messages à la reconnexion

À retenir !

WebSocket

Connexion persistante · bidirectionnelle · temps réel · un seul connect()

Reconnexion

Exponential backoff · heartbeat ping/pong · jamais en boucle infinie

Notifications

WS → Zustand store → composants · toasts 5s · badges persistants

Offline

File d'attente · indicateur de statut · reprise à la reconnexion

🧹 Toujours fermer le WebSocket dans le useEffect cleanup

Questions ?

WebSocket · Pair Programming · Notifications temps réel

Pratiquez avec le projet Bootcode Hub !

useWebSocket → store → toast → badge → mode offline