Une minorité, mais toujours un nombre considérable de projets, nécessitent l'intégration de sockets Web pour fournir une réaction instantanée d'une interface aux modifications sans récupérer les données.
C'est une chose essentielle, et nous n'allons pas en parler ni faire de comparaison entre des bibliothèques tierces qui fournissent des API pour une meilleure expérience de développement.
Mon objectif est de montrer comment intégrer rapidement @microsoft/signalr
avec NextJs. Et comment résoudre les problèmes auxquels nous avons été confrontés lors du développement.
J'espère que tout le monde a déjà installé et déployé le projet NextJS localement. Dans mon cas, la version est 13.2.4
. Ajoutons quelques bibliothèques plus importantes : swr
(version 2.1.5
) pour la récupération de données et un travail ultérieur avec le cache local et @microsoft/signalr
(version 7.0.5
) - API pour les sockets Web.
npm install --save @microsoft/signalr swr
Commençons par créer une simple fonction fetcher
et un nouveau hook appelé useChatData
pour obtenir les données initiales de notre API REST. Il renvoie une liste des messages pour le chat, des champs qui détectent les erreurs et l'état de chargement, et la méthode mutate
qui permet de modifier les données mises en cache.
// hooks/useChatData.ts import useSWR from 'swr'; type Message = { content: string; createdAt: Date; id: string; }; async function fetcher<TResponse>(url: string, config: RequestInit): Promise<TResponse> { const response = await fetch(url, config); if (!response.ok) { throw response; } return await response.json(); } export const useChatData = () => { const { data, error, isLoading, mutate } = useSWR<Message[]>('OUR_API_URL', fetcher); return { data: data || [], isLoading, isError: error, mutate, }; };
Pour tester qu'il fonctionne comme prévu, mettons à jour notre composant de page. Importez notre hook en haut et extrayez-en les données comme dans l'extrait ci-dessous. Si cela fonctionne, vous verrez les données rendues. Comme vous le voyez, c'est assez simple.
// pages/chat.ts import { useChatData } from 'hooks/useChatData'; const Chat: NextPage = () => { const { data } = useChatData(); return ( <div> {data.map(item => ( <div key={item.id}>{item.content}</div> ))} </div> ); };
L'étape suivante nécessite de connecter notre future page aux sockets Web, de détecter les événements NewMessage
et de mettre à jour un cache avec un nouveau message. Je propose de commencer par créer le service socket dans un fichier séparé.
Selon les exemples de la documentation SignalR, nous devons créer une instance de connexion pour une écoute plus approfondie des événements. J'ai également ajouté un objet de connexions pour éviter les doublons et deux assistants pour démarrer/arrêter les connexions.
// api/socket.ts import { HubConnection, HubConnectionBuilder, LogLevel } from '@microsoft/signalr'; let connections = {} as { [key: string]: { type: string; connection: HubConnection; started: boolean } }; function createConnection(messageType: string) { const connectionObj = connections[messageType]; if (!connectionObj) { console.log('SOCKET: Registering on server events ', messageType); const connection = new HubConnectionBuilder() .withUrl('API_URL', { logger: LogLevel.Information, withCredentials: false, }) .withAutomaticReconnect() .build(); connections[messageType] = { type: messageType, connection: connection, started: false, }; return connection; } else { return connections[messageType].connection; } } function startConnection(messageType: string) { const connectionObj = connections[messageType]; if (!connectionObj.started) { connectionObj.connection.start().catch(err => console.error('SOCKET: ', err.toString())); connectionObj.started = true; } } function stopConnection(messageType: string) { const connectionObj = connections[messageType]; if (connectionObj) { console.log('SOCKET: Stoping connection ', messageType); connectionObj.connection.stop(); connectionObj.started = false; } } function registerOnServerEvents( messageType: string, callback: (payload: Message) => void, ) { try { const connection = createConnection(messageType); connection.on('NewIncomingMessage', (payload: Message) => { callback(payload); }); connection.onclose(() => stopConnection(messageType)); startConnection(messageType); } catch (error) { console.error('SOCKET: ', error); } } export const socketService = { registerOnServerEvents, stopConnection, };
Alors maintenant, notre page pourrait ressembler à l’extrait de code. Nous récupérons et extrayons data
avec la liste des messages et les restituons. De plus, useEffect
ci-dessus enregistre l'événement NewMessage
, crée une connexion et écoute le backend.
Lorsque l'événement se déclenche, la méthode mutate
du hook met à jour la liste existante avec un nouvel objet.
// pages/chat.ts import { useChatData } from 'hooks/useChatData'; import { socketService } from 'api/socket'; const Chat: NextPage = () => { const { data } = useChatData(); useEffect(() => { socketService.registerOnServerEvents( 'NewMessage', (payload: Message) => { mutate(() => [...data, payload], { revalidate: false }); } ); }, [data]); useEffect(() => { return () => { socketService.stopConnection('NewMessage'); }; }, []); return ( <div> {data.map(item => ( <div key={item.id}>{item.content}</div> ))} </div> ); };
Cela me semble bien, cela fonctionne et nous voyons comment les nouveaux messages apparaissent dans le flux. J'ai choisi l'exemple basique avec le chat car il est clair et facile à comprendre. Et bien sûr, vous l’appliquez selon votre propre logique.
En utilisant l'une des versions ( @microsoft/signalr
), nous avons été confrontés à un problème de duplications. Il était connecté à useEffect
, le tableau de dépendances. Chaque fois que la dépendance était modifiée, connection.on(event, callback);
rappel mis en cache et déclenché encore et encore.
useEffect(() => { // data equals [] by default (registerOnServerEvents 1 run), // but after initial data fetching it changes (registerOnServerEvents 2 run) // each event changes data and triggers runnning of registerOnServerEvents socketService.registerOnServerEvents( 'NewMessage', // callback cached (payload: Message) => { // mutate called multiple times on each data change mutate(() => [...data, payload], { revalidate: false }); } ); }, [data]); // after getting 3 messages events, we had got 4 messages rendered lol
La solution la plus rapide et la plus fiable que nous avons trouvée consistait à conserver une copie des données dans la ref
React et à l'utiliser dans useEffect
pour les futures mises à jour.
// pages/chat.ts import { useChatData } from 'hooks/useChatData'; import { socketService } from 'api/socket'; const Chat: NextPage = () => { const { data } = useChatData(); const messagesRef = useRef<Message[]>([]); useEffect(() => { messagesRef.current = chatData; }, [chatData]); useEffect(() => { socketService.registerOnServerEvents( 'NewMessage', (payload: Message) => { const messagesCopy = messagesRef.current.slice(); mutate(() => [...messagesCopy, payload], { revalidate: false }); } ); }, [data]); useEffect(() => { return () => { socketService.stopConnection('NewMessage'); }; }, []); return ( <div> {data.map(item => ( <div key={item.id}>{item.content}</div> ))} </div> ); };
Actuellement, nous utilisons une nouvelle version de @microsoft/signalr
qui semble déjà contenir les correctifs nécessaires. Quoi qu’il en soit, si quelqu’un trouve cette solution utile et utilise cette solution de contournement, j’en serai heureux. Pour conclure, je tiens à dire que mon expérience avec SignalR est plutôt positive, l'installation n'a nécessité aucune dépendance ou paramètre spécifique, et cela fonctionne bien et couvre nos besoins.