Dans le développement frontend moderne, la capacité d'afficher des données en direct, mises à jour à la seconde près, n'est plus un luxe mais une attente standard. Des tableaux de bord financiers aux outils d'édition collaborative, les utilisateurs exigent un retour instantané. Alors que le polling est une approche obsolète et inefficace, les WebSockets offrent une communication persistante et bidirectionnelle qui résout ce problème élégamment. Cependant, gérer l'état des WebSockets dans une application React peut devenir rapidement complexe. C'est là que React Query (désormais TanStack Query) brille, offrant une solution robuste pour récupérer et mettre en cache l'état du serveur. Dans cet article, nous explorerons comment combiner ces deux technologies pour créer une expérience temps réel fluide.
Le défi de la synchronisation de l'état
React Query excelle dans la gestion de la récupération de données basée sur les requêtes (HTTP/REST). Il fournit des hooks comme useQuery pour la mise en cache et useMutation pour les effets secondaires. Cependant, les WebSockets fonctionnent sur un modèle piloté par les événements, et non sur un cycle requête-réponse. Ce décalage architectural conduit souvent les développeurs à contourner React Query, gérant manuellement les abonnements WebSocket dans des hooks useEffect. Cette approche devient rapidement ingérable à mesure que l'application évolue, entraînant des conditions de course, des fuites de mémoire et des états d'interface utilisateur incohérents.
L'objectif est de tirer parti des puissants mécanismes de mise en cache et d'invalidation de React Query tout en utilisant les WebSockets comme couche de transport pour les mises à jour. Nous avons besoin d'une stratégie où le WebSocket agit comme un notificateur, déclenchant React Query pour recharger les données ou mettre à jour son cache directement.
Configuration du client WebSocket
D'abord, établissons un client WebSocket simple. Nous créerons un hook personnalisé qui gère le cycle de vie de la connexion. Ce hook doit gérer l'ouverture de la connexion, l'envoi de messages et l'écoute des événements entrants.
import { useEffect, useRef } from 'react';
export const useWebSocket = (url, onMessage) => {
const ws = useRef(null);
useEffect(() => {
ws.current = new WebSocket(url);
ws.current.onopen = () => console.log('Connecté');
ws.current.onmessage = (event) => {
const data = JSON.parse(event.data);
if (onMessage) onMessage(data);
};
ws.current.onclose = () => console.log('Déconnecté');
return () => ws.current.close();
}, [url, onMessage]);
const sendMessage = (message) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(message));
}
};
return sendMessage;
};
Relier WebSockets et React Query
Le modèle le plus efficace consiste à utiliser le WebSocket pour déclencher des invalidations de cache. Lorsqu'un message WebSocket indique que les données ont changé, nous pouvons appeler queryClient.invalidateQueries(). Cela indique à React Query que les données mises en cache sont périmées et doivent être rechargées depuis le serveur via le point de terminaison HTTP standard.
Voici comment vous pouvez intégrer cela dans un composant récupérant des messages de chat en temps réel :
import { useQuery, useQueryClient } from '@tanstack/react-query';
import { useWebSocket } from './useWebSocket';
const fetchMessages = async () => {
const response = await fetch('/api/messages');
return response.json();
};
const RealTimeChat = () => {
const queryClient = useQueryClient();
const { data, isLoading } = useQuery({
queryKey: ['messages'],
queryFn: fetchMessages,
});
// Utiliser WebSocket pour écouter les nouveaux messages
const sendMessage = useWebSocket('wss://api.example.com/chat', (msg) => {
if (msg.type === 'NEW_MESSAGE') {
// Invalider la requête pour recharger les dernières données
queryClient.invalidateQueries({ queryKey: ['messages'] });
// Alternativement, pour une meilleure expérience utilisateur, vous pourriez mettre à jour le cache de manière optimiste :
// queryClient.setQueryData(['messages'], old => [...old, msg.payload]);
}
});
if (isLoading) return <p>Chargement...</p>;
return (
<div>
{data.map(msg => <p key={msg.id}>{msg.text}</p>)}
<button onClick={() => sendMessage({ type: 'SEND', text: 'Bonjour' })}>
Envoyer
</button>
</div>
);
};
Mises à jour optimistes pour une meilleure UX
Bien que l'invalidation fonctionne, elle introduit un bref soubresaut pendant le rechargement des données. Pour une expérience plus fluide, envisagez d'utiliser queryClient.setQueryData() pour les mises à jour optimistes. Lorsqu'un message est reçu, vous pouvez mettre manuellement à jour le cache avec le nouvel élément. Cela fournit un retour immédiat à l'utilisateur sans attendre une requête réseau. N'oubliez pas de toujours garder le serveur comme source de vérité en rechargeant occasionnellement les données pour résoudre d'éventuels conflits.
Conclusion
L'intégration des WebSockets avec React Query n'a pas à être douloureuse. En traitant les WebSockets comme une couche de notification plutôt que comme une couche de récupération de données, vous pouvez maintenir la simplicité et la puissance de la gestion du cache de React Query. Cette approche hybride garantit que vos applications sont à la fois performantes et réactives, offrant les capacités temps réel que les utilisateurs attendent tout en gardant votre base de code maintenable. À mesure que vous adoptez ce modèle, tenez toujours compte de la fiabilité du réseau et mettez en œuvre une logique de reconnexion pour assurer une expérience utilisateur robuste.