WebSockets & Tempo Real
Estratégias de reconexão e fallbacks de polling com consciência de bateria para dados ao vivo no Expo SDK 57 sem consumir a bateria em segundo plano.
Busque em todas as páginas da documentação
Estratégias de reconexão e fallbacks de polling com consciência de bateria para dados ao vivo no Expo SDK 57 sem consumir a bateria em segundo plano.
Cartão de receita de referência rápida - pronto para copiar e colar.
// src/realtime/useReconnectingSocket.ts
import { useEffect, useRef, useState } from "react";
import { AppState, type AppStateStatus } from "react-native";
type Options = {
url: string;
onMessage: (data: unknown) => void;
maxBackoffMs?: number;
};
export function useReconnectingSocket({ url, onMessage, maxBackoffMs = 30_000 }: Options) {
const wsRef = useRef<WebSocket | null>(null);
const attemptRef = useRef(0);
const [connected, setConnected] = useState(false);
const appState = useRef<AppStateStatus>(AppState.currentState);
useEffect(() => {
let timer: ReturnType<typeof setTimeout> | null = null;
let closed = false;
function connect() {
if (closed || appState.current !== "active") return;
const ws = new WebSocket(url);
wsRef.current = ws;
ws.onopen = () => {
attemptRef.current = 0;
setConnected(true);
};
ws.onmessage = (event) => {
try {
onMessage(JSON.parse(String(event.data)));
} catch {
onMessage(event.data);
}
};
ws.onclose = () => {
setConnected(false);
if (closed || appState.current !== "active") return;
const delay = Math.min(1000 * 2 ** attemptRef.current, maxBackoffMs);
attemptRef.current += 1;
timer = setTimeout(connect, delay * (0.5 + Math.random() * 0.5));
};
ws.onerror = () => ws.close();
}
const sub = AppState.addEventListener("change", (next) => {
appState.current = next;
if (next === "active") {
connect();
} else {
wsRef.current?.close();
wsRef.current = null;
setConnected(false);
}
});
connect();
return () => {
closed = true;
if (timer) clearTimeout(timer);
wsRef.current?.close();
sub.remove();
};
}, [url, maxBackoffMs, onMessage]);
return { connected };
}Quando usar isso:
import NetInfo from "@react-native-community/netinfo";
import { useQueryClient } from "@tanstack/react-query";
import { useCallback, useEffect, useRef, useState } from "react";
import { AppState, FlatList, StyleSheet, Text, View } from "react-native";
type Message = { id: string; text: string; ts: number };
const WS_URL = "wss://echo.websocket.events"; // substitua pelo seu gateway
const POLL_MS_FOREGROUND = 15_000;
const POLL_MS_BACKGROUND = 120_000;
export function ChatScreen() {
const queryClient = useQueryClient();
const [messages, setMessages] = useState<Message[]>([]);
const [mode, setMode] = useState<"socket" | "poll">("socket");
const pollRef = useRef<ReturnType<typeof setInterval> | null>(null);
const appendMessage = useCallback(
(msg: Message) => {
setMessages((prev) => [...prev, msg]);
queryClient.setQueryData<Message[]>(["inbox"], (old = []) => [...old, msg]);
},
[queryClient]
);
// Caminho do WebSocket (em primeiro plano)
useEffect(() => {
if (mode !== "socket") return;
let ws: WebSocket | null = null;
let reconnectTimer: ReturnType<typeof setTimeout> | null = null;
let attempt = 0;
let disposed = false;
function connect() {
if (disposed || AppState.currentState !== "active") return;
ws = new WebSocket(WS_URL);
ws.onopen = () => {
attempt = 0;
};
ws.onmessage = (e) => {
appendMessage({
id: String(Date.now()),
text: String(e.data),
ts: Date.now(),
});
};
ws.onclose = () => {
if (disposed) return;
const delay = Math.min(1000 * 2 ** attempt++, 20_000);
reconnectTimer = setTimeout(connect, delay);
};
}
connect();
return () => {
disposed = true;
if (reconnectTimer) clearTimeout(reconnectTimer);
ws?.close();
};
}, [mode, appendMessage]);
// Fallback com consciência de bateria: polling em segundo plano ou por política celular
useEffect(() => {
async function pickMode() {
const net = await NetInfo.fetch();
const background = AppState.currentState !== "active";
const expensive = net.type === "cellular";
if (background || expensive) {
setMode("poll");
} else {
setMode("socket");
}
}
void pickMode();
const appSub = AppState.addEventListener("change", () => void pickMode());
const netSub = NetInfo.addEventListener(() => void pickMode());
return () => {
appSub.remove();
netSub.remove();
};
}, []);
useEffect(() => {
if (pollRef.current) clearInterval(pollRef.current);
if (mode !== "poll") return;
const interval =
AppState.currentState === "active" ? POLL_MS_FOREGROUND : POLL_MS_BACKGROUND;
pollRef.current = setInterval(async () => {
// Substitua pelo seu endpoint REST delta
const res = await fetch("https://jsonplaceholder.typicode.com/comments?_limit=1");
if (!res.ok) return;
const data = await res.json();
appendMessage({
id: `poll-${Date.now()}`,
text: data[0]?.body ?? "poll tick",
ts: Date.now(),
});
}, interval);
return () => {
if (pollRef.current) clearInterval(pollRef.current);
};
}, [mode, appendMessage]);
return (
<View style={styles.screen}>
<Text style={styles.badge}>
Transporte: {mode === "socket" ? "WebSocket" : "polling"} ({mode === "poll" ? "consciente de bateria" : "ao vivo"})
</Text>
<FlatList
data={messages}
keyExtractor={(m) => m.id}
renderItem={({ item }) => <Text style={styles.row}>{item.text}</Text>}
/>
</View>
);
}
const styles = StyleSheet.create({
screen: { flex: 1, padding: 16, backgroundColor: "#fff" },
badge: { fontSize: 12, color: "#64748b", marginBottom: 8 },
row: { paddingVertical: 8, borderBottomWidth: StyleSheet.hairlineWidth, borderColor: "#e2e8f0" },
});npx expo install @react-native-community/netinfo @tanstack/react-queryO que isso demonstra:
setQueryData - uma fonte de caixa de entrada para telas de lista.| Fase | Ação |
|---|---|
onclose / onerror | Fecha o socket, agenda a reconexão |
| Backoff | min(1000 * 2^tentativa, 30s) + jitter |
| Fundo do App | Cancela o timer de reconexão; fecha o socket |
| Primeiro plano do App | Reinicia o contador de tentativas opcionalmente; conecta imediatamente |
| Expiração de autenticação | Envia token de atualização via REST primeiro - veja Ciclo de Vida da Sessão de Autenticação |
// Heartbeat - detecta conexões semi-abertas
const ping = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "ping" }));
}, 25_000);| Estado | Intervalo sugerido | Transporte |
|---|---|---|
| Primeiro plano + Wi‑Fi | WebSocket ou poll de 10–15 s | Tempo Real |
| Primeiro plano + celular | WebSocket ou poll de 30 s | Escolha do produto |
| Segundo plano | poll de 60–300 s ou nenhum | Apenas polling se o negócio exigir |
| Offline | Para ambos | Enfileira eventos localmente |
| Tecnologia | Suporte RN | Notas |
|---|---|---|
| WebSocket Nativo | ✅ Embutido | Menor sobrecarga |
| Cliente Socket.io | ✅ Cliente JS | Protocolo extra + bundle; combine com o servidor |
SSE (EventSource) | ⚠️ Limitado | Frequentemente polyfilled; WS é mais simples no RN |
| Firebase / Supabase realtime | ✅ SDKs | Reconexão gerenciada; dependência de fornecedor |
queryClient.setQueryData<Thread>(["thread", id], (old) => {
if (!old) return old;
return { ...old, messages: [...old.messages, incoming] };
});refetchQueries após onopen para preencher eventos perdidossince=cursor fecham a lacuna - documente com o backenduseState e cache de Query paralelos - escolha Query como fonte da verdadebackground; usar polling ou push em vez disso.since em onopen.message.id.| Alternativa | Usar Quando | Não Usar Quando |
|---|---|---|
| WebSocket + hook de reconexão | Chat, dashboards ao vivo | Atualizações raras - polling é mais simples |
| Long polling | Proxies corporativos bloqueiam WS | Você precisa de latência inferior a um segundo |
| FCM / APNs push | Eventos de segundo plano que precisam ser conhecidos | Indicadores de digitação no aplicativo |
| Intervalo de refetch do TanStack Query | Preços de ações de baixa frequência | Streams de alta frequência |
Sim - WebSocket global está disponível no Hermes. Use wss:// em produção; evite ws:// em texto claro, exceto em desenvolvimento local.
Frequentemente sim para chat em primeiro plano. Para recursos em segundo plano ou apenas em redes tarifadas, degrade para polling mais lento ou notificações push.
Passe um token de curta duração na string de consulta (wss://api.example.com/ws?token=) ou envie um frame de autenticação imediatamente após onopen. Atualize via REST antes da reconexão - Noções Básicas de Autenticação Mobile.
Primeiro plano: 15–30 s. Segundo plano: 2–5 min ou pare completamente e confie no push. Meça a bateria com ferramentas de profiling em Android de nível médio.
Sim para endpoints wss. Autenticação de produção e gateways nativos personalizados precisam de builds de cliente de desenvolvimento - o mesmo que REST.
Versões da Stack: Esta página foi escrita para React 19.2.3, React Native 0.86.0 e Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última atualização: 16 de jul. de 2026