WebSockets avec Socket.IO (React + Node)
Ce cours fait suite aux cours React et Node. On va créer une communication temps‑réel entre un client React et un serveur Node en utilisant Socket.IO.
Objectifs
- Comprendre WebSocket vs HTTP et pourquoi Socket.IO
- Mettre en place Socket.IO côté Node (serveur)
- Connecter React via
socket.io-client - Utiliser
emit/on, broadcast, acknowledgements - Créer une chatroom (join/leave, envoyer/recevoir)
Prérequis
- Backend Node (vous pouvez réutiliser votre projet Hono/Node du cours précédent)
- Frontend React (Vite + React comme dans les cours React)
Installation
Backend (Node):
npm i socket.io
Frontend (React):
npm i socket.io-client
Serveur Node (Hono): initialisation et connexion
On part d’une app Hono et on y branche Socket.IO, en JavaScript uniquement. Référence: With Hono (Node.js) et discussion: #1781. Intégrez ceci dans votre fichier existant src/index.js.
import { Hono } from 'hono';
import { serve } from '@hono/node-server';
import { cors } from 'hono/cors';
import { Server } from 'socket.io';
const app = new Hono();
// CORS HTTP pour vos routes REST
app.use(
'*',
cors({
origin: 'http://localhost:5173',
credentials: true,
allowMethods: ['GET', 'POST', 'PUT', 'OPTIONS']
})
);
app.get('/', (c) => c.text('OK'));
const httpServer = serve({ fetch: app.fetch, port: 3000 }, (info) => {
console.log(`Server is running: http://${info.address}:${info.port}`);
});
// Socket.IO attaché au serveur Hono
const io = new Server(httpServer, {
cors: {
origin: 'http://localhost:5173',
methods: ['GET', 'POST'],
credentials: true
}
// path: '/ws' // optionnel
});
io.on('connection', (socket) => {
console.log('client connecté:', socket.id);
socket.on('disconnect', (reason) => {
console.log('client déconnecté:', socket.id, reason);
});
});
Client React: se connecter au serveur
Créez un client Socket.IO partagé. Cela crée une connexion persistante entre votre navigateur et le serveur.
import { io } from 'socket.io-client';
export const socket = io('http://localhost:3000', {
withCredentials: true,
autoConnect: true
});
Explications rapides:
io(url, options)crée un objetsocketqui maintient une connexion ouverte avec le serveur.withCredentials: trueautorise l’envoi de cookies/credentials pour l'authentification (cookies)autoConnect: trueconnecte automatiquement la socket à l'initialisation.
Dans votre composant (ex: App.jsx), on va écouter des évènements du socket et les nettoyer correctement:
- Un évènement est un message nommé, par exemple
connect,disconnect, oupong. - Un listener (écouteur) est une fonction qu’on attache à un évènement. Quand l’évènement arrive, la fonction s’exécute.
- En React, on enregistre ces écouteurs dans
useEffectet on les désinscrit dans la fonction de nettoyage (cleanup) retournée paruseEffect. Ça évite d’ajouter des doublons à chaque re‑rendu et rend une meilleur perf
import { useEffect, useState } from 'react';
import { socket } from './lib/socket';
export default function App() {
const [connected, setConnected] = useState(socket.connected);
useEffect(() => {
function onConnect() {
setConnected(true);
}
function onDisconnect() {
setConnected(false);
}
socket.on('connect', onConnect);
socket.on('disconnect', onDisconnect);
return () => {
socket.off('connect', onConnect);
socket.off('disconnect', onDisconnect);
};
}, []);
return <div>Socket: {connected ? 'connecté' : 'déconnecté'}</div>;
}
emit / on: échanger des événements
Commençons par un ping/pong. L’objectif est de vérifier que la connexion temps‑réel fonctionne et de comprendre le trajet d’un message:
- Le client envoie un évènement
pingavecsocket.emit('ping', payload). - Le serveur écoute (
io.on('connection'...)puissocket.on('ping', ...)) et répond à ce même client viasocket.emit('pong', ...). - Le serveur peut aussi prévenir les autres clients avec
socket.broadcast.emit('someone:pinged', ...)(broadcast = à tout le monde sauf l’émetteur). - Le client écoute
pongavecsocket.on('pong', handler)et met l’UI à jour. C’est un pattern simple pour tester la latence et le cycle aller‑retour.
io.on('connection', (socket) => {
socket.on('ping', (payload) => {
// Répondre seulement à l’émetteur
socket.emit('pong', { echoed: payload, at: Date.now() });
// Prévenir les autres clients (broadcast)
socket.broadcast.emit('someone:pinged', { from: socket.id });
});
});
function PingButton() {
const [lastPong, setLastPong] = useState(null);
useEffect(() => {
function onPong(data) {
setLastPong(data);
}
socket.on('pong', onPong);
return () => socket.off('pong', onPong);
}, []);
return (
<div>
<button onClick={() => socket.emit('ping', { t: Date.now() })}>Ping</button>
<pre>{JSON.stringify(lastPong, null, 2)}</pre>
</div>
);
}
// client
socket.emit('create:message', { text: 'Hello' }, (ack) => {
if (ack.status === 'ok') console.log('message persisté');
});
// serveur
socket.on('create:message', (data, callback) => {
// persist(data)
callback({ status: 'ok' });
});
Chatroom: join/leave + envoyer/recevoir
Nous allons utiliser les "rooms" pour isoler les conversations.
Idée clé:
- Une room est comme un salon privé. Joindre une room (
socket.join(roomId)) inscrit votre socket dans ce salon. - Envoyer un message à une room (
io.to(roomId).emit(...)) diffuse uniquement aux membres de ce salon. socket.to(roomId).emit(...)envoie aux autres membres de la room (sans renvoyer à l’émetteur).- Flux typique: le client émet
room:join→ le serveur appellesocket.join(roomId)→ tous les membres reçoiventroom:user:joined.
io.on('connection', (socket) => {
socket.on('room:join', (roomId) => {
socket.join(roomId);
socket.to(roomId).emit('room:user:joined', { userId: socket.id });
});
socket.on('room:leave', (roomId) => {
socket.leave(roomId);
socket.to(roomId).emit('room:user:left', { userId: socket.id });
});
socket.on('room:message', ({ roomId, text, username }) => {
const message = {
id: `${socket.id}-${Date.now()}`,
text,
username,
sentAt: Date.now()
};
// Envoyer à tous les membres de la room
io.to(roomId).emit('room:message', message);
});
});
import { useEffect, useState } from 'react';
import { socket } from '../lib/socket';
export function Chat({ roomId, username }) {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
useEffect(() => {
socket.emit('room:join', roomId);
return () => socket.emit('room:leave', roomId);
}, [roomId]);
useEffect(() => {
function onMessage(message) {
setMessages((m) => [...m, message]);
}
socket.on('room:message', onMessage);
return () => socket.off('room:message', onMessage);
}, []);
function sendMessage(e) {
e.preventDefault();
if (!input.trim()) return;
socket.emit('room:message', { roomId, text: input, username });
setInput('');
}
return (
<div>
<ul>
{messages.map((m) => (
<li key={m.id}>
<strong>{m.username}</strong>: {m.text}
</li>
))}
</ul>
<form onSubmit={sendMessage}>
<input
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Votre message"
/>
<button type="submit">Envoyer</button>
</form>
</div>
);
}
Points d’attention
- CORS: gardez
origin,credentialsen phase avec votre front. - Nettoyage:
socket.offdans lesuseEffectpour éviter les doubles listeners. - Reconnexion: Socket.IO gère la reconnexion auto, mais pensez à ré‑adhérer aux rooms au besoin.
- Sécurité: rate limiting + auth (Better Auth) + validation d’inputs côté serveur.
Bonus
- Persister les messages (Drizzle + SQLite) et renvoyer l’historique à la connexion
- Acks pour confirmer la persistance avant d’afficher le message côté client
- Authentifier le socket et limiter l’accès aux rooms (Better Auth)