Cours Vibe coding avec l'IA · Module 7: Clone de LinkedIn

Chat et messagerie en temps réel

5 min de lecture
Dans cette leçon3

Un recruteur écrit à un candidat et attend une réponse tout de suite, pas après un rechargement de la page. Dans le module précédent, la cloche interrogeait le serveur toutes les 30 secondes. Pour un chat, c'est trop lent, et la plupart de ces requêtes reviennent vides. Il nous faut une connexion par laquelle le serveur envoie lui-même un message dès qu'il arrive : c'est le WebSocket.

La connexion WebSocket

Un WebSocket est une connexion permanente et bidirectionnelle entre le navigateur et le serveur, comme une corde tendue entre deux navires : les messages circulent dans les deux sens sans réamarrer. Enfermons-la dans notre propre hook et utilisons-la dans le chat :

1import { useEffect, useState } from 'react';
2
3function useWebSocket(url: string) {
4  const [ws, setWs] = useState<WebSocket | null>(null);
5  const [messages, setMessages] = useState<any[]>([]);
6
7  useEffect(() => {
8    const socket = new WebSocket(url);
9
10    socket.onmessage = (event) => {
11      const message = JSON.parse(event.data);
12      setMessages(prev => [...prev, message]);
13    };
14
15    setWs(socket);
16    return () => socket.close();
17  }, [url]);
18
19  const send = (message: any) => {
20    ws?.send(JSON.stringify(message));
21  };
22
23  return { messages, send };
24}
25
26function Chat({ recipientId }: { recipientId: number }) {
27  const { messages, send } = useWebSocket('ws://localhost:3001');
28  const [input, setInput] = useState("");
29
30  const handleSend = () => {
31    send({ to: recipientId, content: input });
32    setInput("");
33  };
34
35  return (
36    <div className="flex flex-col h-96">
37      <div className="flex-1 overflow-auto p-4 space-y-2">
38        {messages.map((msg, i) => (
39          <div key={i} className={msg.from === recipientId ? "text-left" : "text-right"}>
40            <span className="inline-block bg-blue-600 text-white px-4 py-2 rounded">
41              {msg.content}
42            </span>
43          </div>
44        ))}
45      </div>
46      <div className="flex gap-2 p-4 border-t">
47        <input
48          value={input}
49          onChange={(e) => setInput(e.target.value)}
50          onKeyPress={(e) => e.key === 'Enter' && handleSend()}
51          className="flex-1 border rounded px-3 py-2"
52        />
53        <button onClick={handleSend} className="bg-blue-600 text-white px-4 py-2 rounded">
54          Send
55        </button>
56      </div>
57    </div>
58  );
59}

Le hook useWebSocket ouvre la connexion avec new WebSocket(url) et rassemble les messages entrants dans l'état. Chaque message arrive sous forme de texte dans event.data : socket.onmessage le transforme en objet avec JSON.parse(event.data) et l'ajoute à la liste avec setMessages(prev => [...prev, message]). La fonction send fait l'inverse et appelle JSON.stringify avant l'envoi. La fonction de nettoyage return () => socket.close() ferme la connexion quand le composant disparaît ou que l'adresse change.

Le composant Chat utilise le hook comme n'importe quel autre : il affiche messages, et après un clic sur Send ou un appui sur Entrée, il appelle send et vide le champ. Les messages de l'interlocuteur sont à gauche, les tiens à droite. Quelques remarques pour la route :

  • ws:// est une connexion non chiffrée : sur une page en HTTPS, utilise wss://,
  • send() avant l'ouverture de la connexion lève une erreur : vérifie readyState,
  • onKeyPress est obsolète dans React : utilise plutôt onKeyDown.

L'indicateur de frappe

« Le capitaine écrit… » : un petit détail qui anime beaucoup une conversation. On envoie un signal à chaque frappe et on l'annule après une seconde de silence :

1function Chat() {
2  const [typing, setTyping] = useState(false);
3  const typingTimeout = useRef<NodeJS.Timeout>();
4
5  const handleTyping = () => {
6    send({ type: 'typing', userId: currentUser.id });
7
8    if (typingTimeout.current) {
9      clearTimeout(typingTimeout.current);
10    }
11
12    typingTimeout.current = setTimeout(() => {
13      send({ type: 'stop-typing', userId: currentUser.id });
14    }, 1000);
15  };
16
17  return (
18    <div>
19      {/* Messages */}
20      {typing && <p className="text-sm text-gray-500">User is typing...</p>}
21      <input onChange={handleTyping} />
22    </div>
23  );
24}

L'ordre est le suivant : pendant la frappe dans le champ, handleTyping envoie { type: 'typing' }, efface le délai précédent avec clearTimeout, règle un nouveau setTimeout de 1000 ms et, après une seconde sans frappe, envoie { type: 'stop-typing' }. On garde l'identifiant du délai dans useRef, car il doit survivre entre les rendus et sa modification ne doit pas rafraîchir l'affichage. send et currentUser viennent de l'environnement du composant. Tu appelleras setTyping dans onmessage quand un signal de l'interlocuteur arrivera, car ce fragment ne le fait pas encore.

L'autre bout de la corde

Le navigateur n'est qu'un bout de la connexion. À l'autre bout doit tourner un serveur WebSocket, par exemple en Node.js avec la bibliothèque ws ou Socket.IO, ou un service temps réel externe. L'adresse ws://localhost:3001 de l'exemple désigne justement un tel serveur local. Depuis 2026, Vercel prend en charge les WebSockets dans ses fonctions (en bêta pour l'instant), mais ferme la connexion quand la fonction atteint sa durée maximale. Un réseau faible peut aussi couper la connexion : reconnecte-toi dans l'événement onclose.

Conseil du Capitaine Barbe-Rouge : avant de demander un chat à l'IA, fixe un format de message, par exemple { type, from, to, content }, et respecte-le des deux côtés. La moitié des bugs de chat viennent de navires qui parlent des langues différentes.

À retenir : un WebSocket est une corde permanente entre le navigateur et le serveur, et les messages y circulent en JSON dans les deux sens. Dans l'éditeur ci-dessous, tu as un chat avec le hook useWebSocket, des messages de la forme { type, from, to, content } et un indicateur de saisie. Le socket est simulé, car l'aperçu n'a pas de serveur. Les exercices porteront sur le hook, la réception des messages et l'indicateur de saisie.

Code de cette leçon : App.jsx
1// Chat en temps réel : le hook useWebSocket et l’indicateur de saisie
2import { useState, useEffect, useRef } from 'react';
3
4// L’aperçu n’a pas de serveur, on simule donc le socket : send() reçoit la réponse de l’autre personne après un instant.
5// Le vrai code crée new WebSocket('wss://...') et envoie du JSON de la forme { type, from, to, content }.
6function createFakeSocket(onMessage) {
7  return {
8    send(raw) {
9      const msg = JSON.parse(raw);
10      if (msg.type === 'typing') return;
11      setTimeout(() => onMessage({ type: 'typing', from: 'redbeard' }), 300);
12      setTimeout(
13        () => onMessage({ type: 'message', from: 'redbeard', to: msg.from, content: 'Reçu : ' + msg.content }),
14        1500
15      );
16    },
17    close() {},
18  };
19}
20
21function useWebSocket() {
22  const [messages, setMessages] = useState([]);
23  const [othersTyping, setOthersTyping] = useState(false);
24  const socketRef = useRef(null);
25
26  useEffect(() => {
27    const socket = createFakeSocket((msg) => {
28      if (msg.type === 'typing') {
29        setOthersTyping(true);
30        return;
31      }
32      setOthersTyping(false);
33      setMessages((prev) => [...prev, msg]);
34    });
35    socketRef.current = socket;
36    return () => socket.close();
37  }, []);
38
39  const send = (msg) => socketRef.current?.send(JSON.stringify(msg));
40  return { messages, setMessages, send, othersTyping };
41}
42
43export default function App() {
44  const user = 'me';
45  const { messages, setMessages, send, othersTyping } = useWebSocket();
46  const [text, setText] = useState('');
47  const [typing, setTyping] = useState(false);
48  const typingTimeout = useRef(null);
49
50  // Le signal de saisie part une seule fois, et l’indicateur s’éteint après une seconde de silence.
51  const handleTyping = (e) => {
52    setText(e.target.value);
53    if (!typing) {
54      setTyping(true);
55      send({ type: 'typing', from: user });
56    }
57    clearTimeout(typingTimeout.current);
58    typingTimeout.current = setTimeout(() => setTyping(false), 1000);
59  };
60
61  const handleSend = (e) => {
62    e.preventDefault();
63    if (!text.trim()) return;
64    const message = { type: 'message', from: user, to: 'redbeard', content: text.trim() };
65    setMessages((prev) => [...prev, message]);
66    send(message);
67    setText('');
68  };
69
70  return (
71    <div className="min-h-screen bg-gray-100 flex items-center justify-center p-6">
72      <div className="w-full max-w-md bg-white rounded-xl shadow flex flex-col h-[28rem]">
73        <div className="px-4 py-3 border-b font-semibold">Kapitan Redbeard</div>
74        <div className="flex-1 overflow-y-auto p-4 space-y-2">
75          {messages.length === 0 && <p className="text-sm text-gray-400 text-center">Écris le premier message</p>}
76          {messages.map((m, i) => (
77            <div key={i} className={`flex ${m.from === user ? 'justify-end' : 'justify-start'}`}>
78              <span
79                className={`px-3 py-2 rounded-2xl text-sm max-w-[75%] ${
80                  m.from === user ? 'bg-blue-600 text-white' : 'bg-gray-200 text-gray-900'
81                }`}
82              >
83                {m.content}
84              </span>
85            </div>
86          ))}
87          {othersTyping && <p className="text-xs text-gray-400">Kapitan Redbeard écrit...</p>}
88        </div>
89        <form onSubmit={handleSend} className="p-3 border-t flex gap-2">
90          <input
91            value={text}
92            onChange={handleTyping}
93            placeholder="Message"
94            className="flex-1 border rounded-full px-4 py-2 text-sm"
95          />
96          <button className="px-4 rounded-full bg-blue-600 text-white text-sm font-semibold">Envoyer</button>
97        </form>
98      </div>
99    </div>
100  );
101}
102

Tu as repéré une erreur dans cette leçon ?

Teste tes connaissances

Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.

  1. 1. À quoi sert le hook useWebSocket dans le chat ?

  2. 2. Comment le chat traite-t-il les données entrantes dans socket.onmessage ?

Tâches pratiques dans le jeu

  • Clique dans l'ordre

    Ordonne la création de la connexion dans useWebSocket : new WebSocket(url)

  • Tri vertical

    Ordonne les étapes de l'indicateur de frappe (handleTyping) dans le chat :

Articles utiles