Uma minoria, mas ainda um grande número de projetos, exige integração de web sockets para fornecer reação instantânea de uma interface às alterações sem buscar novamente os dados.
É algo essencial e não vamos falar sobre eles ou fazer uma comparação entre bibliotecas de terceiros que fornecem API para uma melhor experiência de desenvolvimento.
Meu objetivo é mostrar como integrar rapidamente @microsoft/signalr
com NextJs. E como resolver os problemas que enfrentamos durante o desenvolvimento.
Espero que todos já tenham instalado e implantado o projeto NextJS localmente. No meu caso, a versão é 13.2.4
. Vamos adicionar algumas bibliotecas mais importantes: swr
(versão 2.1.5
) para busca de dados e trabalho adicional com o cache local e @microsoft/signalr
(versão 7.0.5
) - API para web sockets.
npm install --save @microsoft/signalr swr
Vamos começar criando uma função fetcher
simples e um novo gancho chamado useChatData
para obter dados iniciais de nossa API REST. Ele retorna uma lista de mensagens para o chat, campos que detectam erros e estado de carregamento, e o método mutate
que permite alterar os dados armazenados em 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, }; };
Para testar se funciona como esperado, vamos atualizar nosso componente de página. Importe nosso gancho na parte superior e extraia os dados dele como no trecho abaixo. Se funcionar, você verá os dados renderizados. Como você pode ver, é bem simples.
// 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> ); };
A próxima etapa requer conectar nossa página futura a web sockets, capturar eventos NewMessage
e atualizar um cache com uma nova mensagem. Proponho começar construindo o serviço de soquete em um arquivo separado.
De acordo com os exemplos nos documentos do SignalR, temos que criar uma instância de conexão para ouvir ainda mais os eventos. Também adicionei um objeto de conexões para evitar duplicatas e dois auxiliares para iniciar/interromper as conexões.
// 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, };
Agora, nossa página pode ter a aparência do trecho de código. Buscamos e extraímos data
com a lista de mensagens e os renderizamos. Além disso, useEffect
acima registra o evento NewMessage
, cria uma conexão e escuta o backend.
Quando o evento é acionado, o método mutate
do gancho atualiza a lista existente com um novo objeto.
// 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> ); };
Parece bom para mim, funciona e vemos como novas mensagens aparecem no feed. Escolhi o exemplo básico com chat porque é claro e fácil de entender. E, claro, você aplica isso de acordo com sua própria lógica.
Utilizando uma das versões ( @microsoft/signalr
), enfrentamos um problema de duplicações. Ele estava conectado a useEffect
, o array de dependências. Cada vez que a dependência foi alterada, connection.on(event, callback);
retorno de chamada em cache e acionou-o repetidamente.
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
A solução mais rápida e confiável que encontramos foi manter uma cópia dos dados dentro da ref
do React e usá-la dentro de useEffect
para atualizações futuras.
// 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> ); };
Atualmente, usamos uma nova versão do @microsoft/signalr
que parece já ter as correções necessárias. Mas de qualquer forma, se alguém achar esta solução útil e usar essa solução alternativa, ficarei feliz. Para finalizar, quero dizer que minha experiência com o SignalR é bastante positiva, a instalação não exigiu nenhuma dependência ou configuração específica e funciona bem e atende às nossas necessidades.