React & Frontend

O que Todo Dev Deve Saber sobre Hooks para WebSockets: Conexão Reativa e Reconexão Automática

12 min de leitura

O que Todo Dev Deve Saber sobre Hooks para WebSockets: Conexão Reativa e Reconexão Automática

Entendendo WebSockets e a Necessidade de Hooks Reativos WebSockets estabelecem uma conexão bidirecional permanente entre cliente e servidor, diferente de HTTP que é requisição-resposta. Essa natureza contínua torna a comunicação em tempo real possível, mas também introduz complexidade no gerenciamento de estado e ciclo de vida da conexão. Em aplicações modernas com frameworks como React ou Vue, precisamos de uma abstração que integre WebSockets ao modelo reativo do framework — é aí que entram os Hooks. Um Hook para WebSocket é, essencialmente, uma função reutilizável que encapsula toda a lógica de conexão, desconexão, envio e recebimento de mensagens, expondo isso de forma declarativa ao componente. Isso elimina boilerplate repetitivo e torna o código mais previsível. Diferente de gerenciar WebSocket diretamente no componente, um Hook cuida dos efeitos colaterais, tratamento de erros e sincronização automática com o ciclo de vida do componente. Arquitetura Fundamental: Estruturando um Hook Reativo Conceitos de Base Um Hook reativo para WebSocket deve ser construído sobre três

<h2>Entendendo WebSockets e a Necessidade de Hooks Reativos</h2>

<p>WebSockets estabelecem uma conexão bidirecional permanente entre cliente e servidor, diferente de HTTP que é requisição-resposta. Essa natureza contínua torna a comunicação em tempo real possível, mas também introduz complexidade no gerenciamento de estado e ciclo de vida da conexão. Em aplicações modernas com frameworks como React ou Vue, precisamos de uma abstração que integre WebSockets ao modelo reativo do framework — é aí que entram os Hooks.</p>

<p>Um Hook para WebSocket é, essencialmente, uma função reutilizável que encapsula toda a lógica de conexão, desconexão, envio e recebimento de mensagens, expondo isso de forma declarativa ao componente. Isso elimina boilerplate repetitivo e torna o código mais previsível. Diferente de gerenciar WebSocket diretamente no componente, um Hook cuida dos efeitos colaterais, tratamento de erros e sincronização automática com o ciclo de vida do componente.</p>

<h2>Arquitetura Fundamental: Estruturando um Hook Reativo</h2>

<h3>Conceitos de Base</h3>

<p>Um Hook reativo para WebSocket deve ser construído sobre três pilares: <strong>conexão gerenciada</strong>, <strong>estado reativo</strong> e <strong>efeitos colaterais controlados</strong>. A conexão não deve ser recriada a cada render, o estado deve refletir mudanças em tempo real, e os efeitos devem ser limpos quando o componente é desmontado. Isso previne vazamento de memória e comportamentos impredizíveis.</p>

<p>Vamos começar com a estrutura fundamental em React, que é o framework mais comum para esse padrão:</p>

<pre><code class="language-javascript">import { useEffect, useRef, useState, useCallback } from &#039;react&#039;;

export const useWebSocket = (url) =&gt; {

const wsRef = useRef(null);

const [isConnected, setIsConnected] = useState(false);

const [lastMessage, setLastMessage] = useState(null);

const [data, setData] = useState([]);

useEffect(() =&gt; {

// Criar a conexão apenas uma vez

const ws = new WebSocket(url);

ws.onopen = () =&gt; {

setIsConnected(true);

console.log(&#039;WebSocket conectado&#039;);

};

ws.onmessage = (event) =&gt; {

const parsedData = JSON.parse(event.data);

setLastMessage(parsedData);

setData((prev) =&gt; [...prev, parsedData]);

};

ws.onerror = (error) =&gt; {

console.error(&#039;Erro WebSocket:&#039;, error);

setIsConnected(false);

};

ws.onclose = () =&gt; {

setIsConnected(false);

console.log(&#039;WebSocket desconectado&#039;);

};

wsRef.current = ws;

// Cleanup: desconectar quando componente desmonta

return () =&gt; {

if (wsRef.current &amp;&amp; wsRef.current.readyState === WebSocket.OPEN) {

wsRef.current.close();

}

};

}, [url]);

const send = useCallback((message) =&gt; {

if (wsRef.current &amp;&amp; wsRef.current.readyState === WebSocket.OPEN) {

wsRef.current.send(JSON.stringify(message));

} else {

console.warn(&#039;WebSocket não está pronto para enviar mensagens&#039;);

}

}, []);

return { isConnected, lastMessage, data, send };

};</code></pre>

<p>Este Hook básico oferece: (1) gerenciamento de conexão com useRef para evitar recriações, (2) estado reativo que reflete o status e mensagens recebidas, e (3) função <code>send</code> encapsulada com validação de estado.</p>

<h2>Reconexão Automática e Resiliência</h2>

<h3>Estratégia de Retry com Backoff Exponencial</h3>

<p>Uma aplicação real não pode simplesmente desistir quando a conexão cair. É necessário implementar lógica de reconexão automática, idealmente com backoff exponencial para não sobrecarregar o servidor. Isso significa: primeira tentativa imediata, segunda após 1s, terceira após 2s, e assim por diante até um máximo.</p>

<pre><code class="language-javascript">import { useEffect, useRef, useState, useCallback } from &#039;react&#039;;

export const useWebSocketWithReconnect = (url, options = {}) =&gt; {

const {

maxRetries = 5,

initialDelay = 1000,

maxDelay = 30000,

} = options;

const wsRef = useRef(null);

const reconnectTimerRef = useRef(null);

const [isConnected, setIsConnected] = useState(false);

const [lastMessage, setLastMessage] = useState(null);

const [retryCount, setRetryCount] = useState(0);

const [data, setData] = useState([]);

const calculateDelay = useCallback((attempt) =&gt; {

const exponentialDelay = Math.min(

initialDelay * Math.pow(2, attempt),

maxDelay

);

// Adicionar jitter para evitar thundering herd

const jitter = Math.random() 0.1 exponentialDelay;

return exponentialDelay + jitter;

}, [initialDelay, maxDelay]);

const connect = useCallback(() =&gt; {

try {

const ws = new WebSocket(url);

ws.onopen = () =&gt; {

setIsConnected(true);

setRetryCount(0); // Reset retry count ao conectar

console.log(&#039;WebSocket conectado com sucesso&#039;);

};

ws.onmessage = (event) =&gt; {

try {

const parsedData = JSON.parse(event.data);

setLastMessage(parsedData);

setData((prev) =&gt; [...prev, parsedData]);

} catch (error) {

console.error(&#039;Erro ao fazer parse da mensagem:&#039;, error);

}

};

ws.onerror = (error) =&gt; {

console.error(&#039;Erro WebSocket:&#039;, error);

};

ws.onclose = () =&gt; {

setIsConnected(false);

// Tentar reconectar se ainda houver tentativas disponíveis

if (retryCount &lt; maxRetries) {

const delay = calculateDelay(retryCount);

console.log(Reconectando em ${delay.toFixed(0)}ms (tentativa ${retryCount + 1}/${maxRetries}));

reconnectTimerRef.current = setTimeout(() =&gt; {

setRetryCount((prev) =&gt; prev + 1);

connect();

}, delay);

} else {

console.error(&#039;Máximo de tentativas de reconexão atingido&#039;);

}

};

wsRef.current = ws;

} catch (error) {

console.error(&#039;Erro ao criar WebSocket:&#039;, error);

}

}, [url, retryCount, maxRetries, calculateDelay]);

useEffect(() =&gt; {

connect();

return () =&gt; {

// Cleanup: limpar timer e fechar conexão

if (reconnectTimerRef.current) {

clearTimeout(reconnectTimerRef.current);

}

if (wsRef.current) {

wsRef.current.close();

}

};

}, [connect]);

const send = useCallback((message) =&gt; {

if (wsRef.current &amp;&amp; wsRef.current.readyState === WebSocket.OPEN) {

wsRef.current.send(JSON.stringify(message));

return true;

} else {

console.warn(&#039;WebSocket não está pronto. Status:&#039;, wsRef.current?.readyState);

return false;

}

}, []);

const disconnect = useCallback(() =&gt; {

if (reconnectTimerRef.current) {

clearTimeout(reconnectTimerRef.current);

}

if (wsRef.current) {

wsRef.current.close();

}

}, []);

return {

isConnected,

lastMessage,

data,

send,

disconnect,

retryCount,

isRetrying: retryCount &gt; 0 &amp;&amp; !isConnected,

};

};</code></pre>

<p>Esta versão melhorada adiciona: (1) cálculo dinâmico de delay com backoff exponencial, (2) jitter para evitar ressincronização simultânea de múltiplos clientes, (3) limite de tentativas configurável, (4) rastreamento de quantas tentativas foram feitas, e (5) funções <code>disconnect</code> para forçar desconexão quando necessário.</p>

<h2>Implementação Prática em Componentes</h2>

<h3>Caso de Uso Real: Chat em Tempo Real</h3>

<p>Agora vamos integrar nosso Hook em um componente real. Considere um chat onde mensagens chegam em tempo real:</p>

<pre><code class="language-javascript">import React, { useState } from &#039;react&#039;;

import { useWebSocketWithReconnect } from &#039;./hooks/useWebSocketWithReconnect&#039;;

export const ChatComponent = ({ userId }) =&gt; {

const [inputMessage, setInputMessage] = useState(&#039;&#039;);

const { isConnected, data, send, isRetrying } = useWebSocketWithReconnect(

wss://api.example.com/chat/${userId},

{ maxRetries: 5, initialDelay: 1000 }

);

const handleSendMessage = (e) =&gt; {

e.preventDefault();

if (!inputMessage.trim()) return;

const success = send({

type: &#039;message&#039;,

text: inputMessage,

timestamp: new Date().toISOString(),

userId,

});

if (success) {

setInputMessage(&#039;&#039;);

} else {

alert(&#039;Falha ao enviar. Reconectando...&#039;);

}

};

return (

&lt;div className=&quot;chat-container&quot;&gt;

&lt;div className=&quot;status-bar&quot;&gt;

{isConnected ? (

&lt;span className=&quot;status-connected&quot;&gt;🟢 Conectado&lt;/span&gt;

) : isRetrying ? (

&lt;span className=&quot;status-retrying&quot;&gt;🟡 Reconectando...&lt;/span&gt;

) : (

&lt;span className=&quot;status-disconnected&quot;&gt;🔴 Desconectado&lt;/span&gt;

)}

&lt;/div&gt;

&lt;div className=&quot;messages&quot;&gt;

{data.map((msg, idx) =&gt; (

&lt;div key={idx} className=&quot;message&quot;&gt;

&lt;strong&gt;{msg.userId}:&lt;/strong&gt; {msg.text}

&lt;small&gt;{new Date(msg.timestamp).toLocaleTimeString()}&lt;/small&gt;

&lt;/div&gt;

))}

&lt;/div&gt;

&lt;form onSubmit={handleSendMessage} className=&quot;input-form&quot;&gt;

&lt;input

type=&quot;text&quot;

value={inputMessage}

onChange={(e) =&gt; setInputMessage(e.target.value)}

placeholder=&quot;Digite uma mensagem...&quot;

disabled={!isConnected}

/&gt;

&lt;button type=&quot;submit&quot; disabled={!isConnected || !inputMessage.trim()}&gt;

Enviar

&lt;/button&gt;

&lt;/form&gt;

&lt;/div&gt;

);

};</code></pre>

<p>Aqui o Hook simplifica enormemente o componente. Não há necessidade de gerenciar timers, estados de reconexão ou tratamento manual de ciclo de vida — tudo é encapsulado. O componente apenas se preocupa em renderizar a UI baseado no estado fornecido pelo Hook.</p>

<h3>Avançado: Hook com Context para Múltiplos Componentes</h3>

<p>Em aplicações maiores, pode ser necessário compartilhar a conexão entre vários componentes. Nesse caso, combinamos o Hook com Context:</p>

<pre><code class="language-javascript">import React, { createContext, useContext } from &#039;react&#039;;

import { useWebSocketWithReconnect } from &#039;./useWebSocketWithReconnect&#039;;

const WebSocketContext = createContext(null);

export const WebSocketProvider = ({ url, children, options }) =&gt; {

const wsState = useWebSocketWithReconnect(url, options);

return (

&lt;WebSocketContext.Provider value={wsState}&gt;

{children}

&lt;/WebSocketContext.Provider&gt;

);

};

export const useWebSocket = () =&gt; {

const context = useContext(WebSocketContext);

if (!context) {

throw new Error(&#039;useWebSocket deve ser usado dentro de WebSocketProvider&#039;);

}

return context;

};</code></pre>

<p>E seu uso em App:</p>

<pre><code class="language-javascript">function App() {

return (

&lt;WebSocketProvider

url=&quot;wss://api.example.com/notifications&quot;

options={{ maxRetries: 5, initialDelay: 1000 }}

&gt;

&lt;ChatComponent userId=&quot;user123&quot; /&gt;

&lt;NotificationPanel /&gt;

&lt;StatusIndicator /&gt;

&lt;/WebSocketProvider&gt;

);

}</code></pre>

<p>Agora qualquer componente dentro da árvore pode usar <code>useWebSocket()</code> sem repeti-lo. A conexão é única e gerenciada centralmente, economizando recursos e simplificando sincronização.</p>

<h2>Padrões Avançados e Otimizações</h2>

<h3>Tratamento de Backpressure e Fila de Mensagens</h3>

<p>Em alguns cenários, o servidor pode estar lento para processar mensagens, ou o cliente envia mais rápido que o servidor consegue receber. É prudente implementar uma fila:</p>

<pre><code class="language-javascript">const useWebSocketWithQueue = (url, options = {}) =&gt; {

const { queueSize = 100 } = options;

const messageQueueRef = useRef([]);

const isProcessingRef = useRef(false);

const { isConnected, send: baseSend, ...rest } = useWebSocketWithReconnect(url, options);

const processQueue = useCallback(() =&gt; {

if (isProcessingRef.current || !isConnected || messageQueueRef.current.length === 0) {

return;

}

isProcessingRef.current = true;

const message = messageQueueRef.current.shift();

const success = baseSend(message);

if (success) {

isProcessingRef.current = false;

// Processar próxima mensagem após pequeno delay

setTimeout(processQueue, 50);

} else {

// Se falhar, recolocar na fila

messageQueueRef.current.unshift(message);

isProcessingRef.current = false;

}

}, [isConnected, baseSend]);

useEffect(() =&gt; {

processQueue();

}, [isConnected, processQueue]);

const send = useCallback((message) =&gt; {

if (messageQueueRef.current.length &lt; queueSize) {

messageQueueRef.current.push(message);

processQueue();

return true;

} else {

console.warn(&#039;Fila de mensagens cheia, descartando mensagem&#039;);

return false;

}

}, [processQueue, queueSize]);

return { isConnected, send, queueLength: messageQueueRef.current.length, ...rest };

};</code></pre>

<p>Este padrão garante que mensagens não se perdem e são processadas de forma ordenada, mesmo sob alta carga.</p>

<h3>Heartbeat para Detectar Conexões Mortas</h3>

<p>Algumas conexões WebSocket podem &quot;morrer&quot; silenciosamente sem dispara o evento <code>onclose</code>. Um heartbeat ajuda a detectar isso:</p>

<pre><code class="language-javascript">const useWebSocketWithHeartbeat = (url, options = {}) =&gt; {

const { heartbeatInterval = 30000 } = options;

const heartbeatTimerRef = useRef(null);

const wsHook = useWebSocketWithReconnect(url, options);

const { isConnected, send } = wsHook;

useEffect(() =&gt; {

if (!isConnected) return;

heartbeatTimerRef.current = setInterval(() =&gt; {

const success = send({ type: &#039;ping&#039;, timestamp: Date.now() });

if (!success) {

console.warn(&#039;Falha ao enviar heartbeat, desconectando...&#039;);

clearInterval(heartbeatTimerRef.current);

}

}, heartbeatInterval);

return () =&gt; {

if (heartbeatTimerRef.current) {

clearInterval(heartbeatTimerRef.current);

}

};

}, [isConnected, send, heartbeatInterval]);

return wsHook;

};</code></pre>

<p>O servidor deve responder com <code>pong</code> quando receber <code>ping</code>. Se não responder dentro de um tempo limite, o cliente desconecta e reconecta automaticamente.</p>

<h2>Conclusão</h2>

<p>Hooks para WebSockets elevam o nível de abstração, permitindo que você trate comunicação em tempo real como um recurso reativo declarativo, similar a qualquer outro estado no seu componente. Os três pontos principais aprendidos foram: <strong>(1) encapsulamento de lógica de conexão</strong> em uma função reutilizável elimina boilerplate e reduz erros, <strong>(2) reconexão automática com backoff exponencial</strong> garante resiliência sem intervenção manual, e <strong>(3) padrões como fila de mensagens e heartbeat</strong> preparam sua aplicação para cenários do mundo real onde conexões são instáveis e carga é imprevisível. Domine esses conceitos e você construirá aplicações em tempo real robustas e escaláveis.</p>

<h2>Referências</h2>

<ul>

<li><a href="https://developer.mozilla.org/en-US/docs/Web/API/WebSocket" target="_blank" rel="noopener noreferrer">MDN Web Docs - WebSocket API</a></li>

<li><a href="https://react.dev/reference/react" target="_blank" rel="noopener noreferrer">React Hooks Official Documentation</a></li>

<li><a href="https://socket.io/docs/v4/client-api/#Socket" target="_blank" rel="noopener noreferrer">Socket.IO Documentation - Automatic Reconnection</a></li>

<li><a href="https://martinfowler.com/articles/patterns-of-distributed-systems/" target="_blank" rel="noopener noreferrer">Martin Fowler - Real-time Web Communication Patterns</a></li>

<li><a href="https://hpbn.co/" target="_blank" rel="noopener noreferrer">High Performance Browser Networking - Ilya Grigorik (Capítulo sobre WebSockets)</a></li>

</ul>

<p>&lt;!-- FIM --&gt;</p>

Comentários

Mais em React & Frontend

O que Todo Dev Deve Saber sobre React Server Components: Modelo Mental e Casos de Uso Reais
O que Todo Dev Deve Saber sobre React Server Components: Modelo Mental e Casos de Uso Reais

O que são React Server Components? React Server Components (RSCs) representam...

React Fiber: Arquitetura Interna, Reconciliation e Rendering Phases: Do Básico ao Avançado
React Fiber: Arquitetura Interna, Reconciliation e Rendering Phases: Do Básico ao Avançado

React Fiber: Arquitetura Interna, Reconciliation e Rendering Phases React Fib...

Boas Práticas de React Hook Form Avançado: Arrays, Nested Fields e Wizards para Times Ágeis
Boas Práticas de React Hook Form Avançado: Arrays, Nested Fields e Wizards para Times Ágeis

Entendendo o Problema: Por Que React Hook Form Avançado? Quando começamos a t...