Interfaces modernas raramente ficam paradas esperando o usuário apertar F5. Notificações, cotações, placares, logs de deploy, tokens de um LLM chegando aos poucos — tudo isso pede que o servidor empurre os dados para o navegador assim que eles existem. É o que chamo informalmente de “tunelamento”: abrir um canal por onde o backend joga eventos direto na tela. Neste artigo comparo duas formas maduras de fazer isso no React — Server-Sent Events (SSE) e SignalR — e mostro código prático de cada uma.
Server-Sent Events: streaming unidirecional sobre HTTP
SSE é um padrão do próprio navegador. O cliente abre uma conexão HTTP persistente com a API EventSource, e o servidor mantém essa conexão aberta enviando mensagens no formato text/event-stream. É um fluxo de mão única: só do servidor para o cliente.
Dois pontos que a documentação da MDN e do web.dev deixam claros e que fazem o SSE brilhar:
- Reconexão automática. Se a conexão cai, o navegador tenta reabrir sozinho (por volta de 3 segundos por padrão). O servidor pode ajustar esse intervalo com o campo
retry: e retomar de onde parou usando o id: de cada evento.
- Zero protocolo novo. Roda sobre HTTP comum, passa por proxies e infraestrutura HTTP sem cerimônia.
Atenção a um limite histórico: sobre HTTP/1.1, os navegadores permitem no máximo ~6 conexões SSE simultâneas por domínio (contando todas as abas). Sob HTTP/2 esse teto sobe bastante, porque as requisições são multiplexadas.
Consumindo um SSE no React
O padrão é criar o EventSource dentro de um useEffect e sempre limpar a conexão no retorno. Use onmessage para eventos sem nome e addEventListener para eventos nomeados (o campo event: enviado pelo servidor).
import { useEffect, useState } from "react";
function Cotacoes() {
const [precos, setPrecos] = useState([]);
useEffect(() => {
const source = new EventSource("/api/cotacoes");
// Evento padrão (sem campo "event:" no servidor)
source.onmessage = (e) => {
const dado = JSON.parse(e.data);
setPrecos((atual) => [dado, ...atual].slice(0, 20));
};
// Evento nomeado: servidor envia "event: alerta"
source.addEventListener("alerta", (e) => {
console.warn("Alerta:", e.data);
});
source.onerror = (err) => {
console.error("Falha no EventSource:", err);
// O navegador já tenta reconectar sozinho.
};
return () => source.close(); // fecha ao desmontar
}, []);
return (
<ul>
{precos.map((p, i) => (
<li key={i}>{p.ativo}: {p.valor}</li>
))}
</ul>
);
}
export default Cotacoes;
Repare que não há lógica de reconexão manual: o EventSource cuida disso. Você só fecha explicitamente com source.close() quando o componente sai de cena.
SignalR: canal bidirecional com fallback automático
SignalR é a biblioteca de tempo real do ASP.NET Core. Diferente do SSE, ela é bidirecional: o cliente chama métodos no servidor (o “hub”) e o servidor chama métodos no cliente. Por baixo, o SignalR escolhe o melhor transporte disponível — de preferência WebSockets, com fallback para Server-Sent Events e, por último, Long Polling. Ou seja: o SSE pode ser até o encanamento interno do SignalR quando WebSocket não está disponível.
No cliente JavaScript/TypeScript você usa o pacote oficial @microsoft/signalr:
npm install @microsoft/signalr
Conectando a um hub no React
Monte a conexão com HubConnectionBuilder, registre os handlers com connection.on(...) antes de chamar start() (boa prática recomendada pela Microsoft) e habilite a reconexão com withAutomaticReconnect().
import { useEffect, useRef, useState } from "react";
import * as signalR from "@microsoft/signalr";
function Chat() {
const [mensagens, setMensagens] = useState([]);
const conexaoRef = useRef(null);
useEffect(() => {
const connection = new signalR.HubConnectionBuilder()
.withUrl("/chatHub")
.withAutomaticReconnect() // 0, 2, 10 e 30s por padrão
.configureLogging(signalR.LogLevel.Information)
.build();
// Servidor chama: Clients.All.SendAsync("ReceiveMessage", user, msg)
connection.on("ReceiveMessage", (user, message) => {
setMensagens((atual) => [...atual, `${user}: ${message}`]);
});
connection.onreconnecting(() => console.log("Reconectando..."));
connection.onreconnected(() => console.log("Reconectado!"));
connection
.start()
.then(() => console.log("SignalR conectado."))
.catch((err) => console.error(err));
conexaoRef.current = connection;
return () => { connection.stop(); };
}, []);
const enviar = async (user, texto) => {
try {
// Chama um método público do hub no servidor
await conexaoRef.current.invoke("SendMessage", user, texto);
} catch (err) {
console.error(err);
}
};
return (
<ul>
{mensagens.map((m, i) => (
<li key={i}>{m}</li>
))}
</ul>
);
}
export default Chat;
Do lado do cliente há duas formas de falar com o servidor: invoke, que retorna uma Promise resolvida com o valor de retorno do método do hub, e send, que apenas dispara a mensagem sem esperar resposta. Para receber, sempre connection.on("NomeDoMetodo", ...).
Sobre reconexão: sem parâmetros, withAutomaticReconnect() tenta reconectar aos 0, 2, 10 e 30 segundos e desiste após quatro falhas. Você pode passar um array de intervalos ou um IRetryPolicy customizado. E atenção: ele não retenta falhas do start() inicial — isso você trata na mão.
SSE x SignalR: qual escolher?
| Critério |
Server-Sent Events |
SignalR |
| Direção |
Unidirecional (servidor → cliente) |
Bidirecional (cliente ↔ servidor) |
| Transporte |
HTTP (text/event-stream) |
WebSockets, com fallback para SSE e Long Polling |
| Cliente |
Nativo (EventSource), sem lib |
Pacote @microsoft/signalr |
| Reconexão |
Automática pelo navegador |
withAutomaticReconnect() (opt-in) |
| Servidor |
Qualquer stack HTTP |
Acoplado ao ASP.NET Core / Azure SignalR |
| Melhor para |
Feeds, notificações, streaming de texto |
Chat, colaboração, jogos, RPC em tempo real |
A regra prática: se o fluxo é só do servidor para a tela e você quer simplicidade máxima sem depender de uma stack específica, SSE resolve com elegância — e sem instalar nada no front. Se você precisa de via dupla, chamadas RPC entre cliente e servidor, grupos, presença e um degradê robusto de transporte, especialmente num backend .NET, o SignalR entrega tudo pronto. Não são rivais absolutos: o SignalR inclusive usa SSE como um de seus transportes internos.
Comece pelo mais simples que atende ao caso de uso. Muita gente sobe um WebSocket completo para o que um humilde EventSource resolveria em dez linhas.
Fontes