WebSockets y Tiempo Real
Estrategias de reconexión y alternativas de polling conscientes de la batería para datos en vivo en Expo SDK 57 sin agotar la batería en segundo plano.
Busca en todas las páginas de la documentación
Estrategias de reconexión y alternativas de polling conscientes de la batería para datos en vivo en Expo SDK 57 sin agotar la batería en segundo plano.
Tarjeta de receta de referencia rápida - lista para copiar y pegar.
// 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 };
}Cuándo usarlo:
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"; // replace with your 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]
);
// WebSocket path (foreground)
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]);
// Alternativa consciente de la batería: polling cuando está en segundo plano o en 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 () => {
// Replace with your REST delta endpoint
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}>
Transport: {mode === "socket" ? "WebSocket" : "polling"} ({mode === "poll" ? "battery-aware" : "live"})
</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-queryLo que esto demuestra:
setQueryData - una fuente de inbox para pantallas de lista.| Fase | Acción |
|---|---|
onclose / onerror | Cierra socket, programa reconexión |
| Backoff | min(1000 * 2^attempt, 30s) + jitter |
| App en segundo plano | Cancela temporizador de reconexión; cierra socket |
| App en primer plano | Reinicia contador de intentos opcional; conecta inmediatamente |
| Expiración de autenticación | Envía token de actualización vía REST primero - ve Auth Session |
// Heartbeat - detectar conexiones medio abiertas
const ping = setInterval(() => {
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: "ping" }));
}, 25_000);| Estado | Intervalo sugerido | Transporte |
|---|---|---|
| Primer plano + Wi-Fi | WebSocket o polling cada 10-15 s | Tiempo real |
| Primer plano + celular | WebSocket o polling cada 30 s | Decisión de producto |
| Segundo plano | Polling cada 60-300 s o nada | Solo polling si el negocio lo requiere |
| Desconectado | Detén ambos | Encola eventos localmente |
| Tecnología | Soporte RN | Notas |
|---|---|---|
| WebSocket Nativo | - Integrado | Menor sobrecarga |
| Cliente Socket.io | - Cliente JS | Protocolo extra + bundle; debe coincidir con servidor |
SSE (EventSource) | - Limitado | A menudo se usa con polyfill; WS es más simple en RN |
| Firebase / Supabase realtime | - SDKs | Reconexión gestionada; vendor lock-in |
queryClient.setQueryData<Thread>(["thread", id], (old) => {
if (!old) return old;
return { ...old, messages: [...old.messages, incoming] };
});refetchQueries después de onopen para rellenar eventos perdidossince=cursor cierran la brecha - documenta con el backenduseState paralelo y caché de Query - elige Query como fuente de verdadbackground; en su lugar, usa polling o push.since en onopen.message.id.| Alternativa | Úsalo cuando | No lo uses cuando |
|---|---|---|
| WebSocket + hook de reconexión | Chat, dashboards en directo | Actualizaciones raras - polling es más simple |
| Long polling | Proxies corporativos bloquean WS | Necesitas latencia de subsegundos |
| FCM / APNs push | Eventos de debe-saber en segundo plano | Indicadores de escritura en la app |
| Intervalo de refetch de TanStack Query | Precios de acciones de baja frecuencia | Transmisiones de alta frecuencia |
Sí - el WebSocket global está disponible en Hermes. Usa wss:// en producción; evita ws:// en texto plano excepto en desarrollo local.
A menudo sí para chat en primer plano. Para características solo en segundo plano o medidas, degrada a polling más lento o notificaciones push.
Pasa un token de corta duración en la cadena de consulta (wss://api.example.com/ws?token=) o envía un marco de autenticación inmediatamente después de onopen. Actualiza vía REST antes de reconectar - Mobile Auth Basics.
Primer plano: 15-30 s. Segundo plano: 2-5 min o detente completamente y confía en push. Mide la batería con herramientas de perfilado en Android de gama media.
Sí para endpoints wss. La autenticación de producción y puertas de enlace nativas personalizadas necesitan compilaciones dev-client - igual que REST.
Versiones de stack: Esta página fue escrita para React 19.2.3, React Native 0.86.0, y Expo SDK 57 (
expo~57.0.4).
Revisado por Chris St. John·Última actualización: 16 jul 2026