WebSockets are how React Native apps get real time: chat, live prices, multiplayer state, collaborative editing. The good news is that React Native ships a browser-compatible WebSocket API out of the box. The bad news is that the happy-path code you find in most tutorials falls over the moment a phone does phone things: backgrounding, network switches, elevators. This guide covers the API, then the parts that actually make it production-ready.
The basics: WebSocket is built in
No library needed. React Native implements the same WebSocket interface you know from the browser:
const ws = new WebSocket('wss://example.com/socket');
ws.onopen = () => {
ws.send(JSON.stringify({ type: 'hello' }));
};
ws.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('received', data);
};
ws.onerror = (e) => console.log('error', e.message);
ws.onclose = (e) => console.log('closed', e.code, e.reason);
This works identically on iOS and Android. Always use wss:// in production; both platforms restrict cleartext traffic by default, and you should not fight that.
A useWebSocket hook
Raw event handlers scattered through components get messy fast. A hook centralizes the connection:
import { useEffect, useRef, useState, useCallback } from 'react';
export function useWebSocket(url) {
const ws = useRef(null);
const [status, setStatus] = useState('connecting');
const [lastMessage, setLastMessage] = useState(null);
useEffect(() => {
const socket = new WebSocket(url);
ws.current = socket;
socket.onopen = () => setStatus('open');
socket.onmessage = (e) => setLastMessage(JSON.parse(e.data));
socket.onclose = () => setStatus('closed');
return () => socket.close(1000, 'unmount');
}, [url]);
const send = useCallback((obj) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify(obj));
}
}, []);
return { status, lastMessage, send };
}
This is fine for a demo. Production needs three more things: reconnection, liveness detection, and app-state awareness.
Reconnection with exponential backoff
Mobile connections die constantly, and a naive reconnect loop will hammer your server and drain the battery. Back off exponentially, with jitter so a thousand clients do not reconnect in lockstep after an outage:
function reconnectDelay(attempt) {
const base = Math.min(30000, 1000 * 2 ** attempt); // cap at 30s
return base / 2 + Math.random() * (base / 2); // add jitter
}
socket.onclose = (e) => {
if (e.code === 1000) return; // clean close, do not reconnect
const delay = reconnectDelay(attempt++);
setTimeout(connect, delay);
};
Reset the attempt counter on a successful open. And distinguish deliberate closes (logout, unmount) from network failures, or your app will busily reconnect sockets nobody wants.
Heartbeats: detecting dead connections
The cruel part of TCP on mobile networks is half-open connections: the socket looks open, but nothing is flowing. Servers and carriers silently drop idle connections. The fix is an application-level ping:
const HEARTBEAT_MS = 25000;
let heartbeat;
socket.onopen = () => {
heartbeat = setInterval(() => {
send({ type: 'ping' });
// if no pong arrives within 5s, force close -> triggers reconnect
pongTimer = setTimeout(() => socket.close(4000, 'no pong'), 5000);
}, HEARTBEAT_MS);
};
Keep the interval under 30 seconds; several mobile carriers and load balancers reap idle connections around the 30 to 60 second mark.
App state: the mobile-only problem
Web tutorials never mention this because browsers do not do it: mobile OSes suspend your JavaScript when the app goes to background. Your socket dies and your timers stop. Listen to AppState and treat foregrounding as a reconnect trigger:
import { AppState } from 'react-native';
AppState.addEventListener('change', (state) => {
if (state === 'active') {
reconnectNow(); // returning to foreground
refetchMissedData(); // fill the gap the socket missed
}
});
That second line matters more than the first. You cannot rely on a background socket for delivery on mobile. The standard architecture is: WebSocket for foreground real time, push notifications for background events, and a fetch-missed-messages call on every reconnect. Plan your server API around that gap-fill from day one.
Offline queueing
When a user hits send in a tunnel, the message should go out when connectivity returns, not vanish. Keep an outbox:
const outbox = [];
function sendReliable(obj) {
if (ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify(obj));
else outbox.push(obj);
}
socket.onopen = () => {
while (outbox.length) ws.send(JSON.stringify(outbox.shift()));
};
For anything that must survive an app kill, persist the outbox (MMKV or SQLite) and add server-side deduplication via client-generated message IDs, because reconnect retries will occasionally double-send.
Libraries worth knowing
- socket.io-client: works in React Native, brings rooms, auto-reconnect, and fallbacks; requires a Socket.IO server, it is not a plain WebSocket protocol.
- graphql-ws: if you are on GraphQL, subscriptions ride on WebSockets with the connection management handled.
- Plain WebSocket + your own layer: what this guide sketches. Maximum control, and now you maintain it.
When to stop building and use an SDK
Everything above is the transport layer. If what you are actually building is chat, the transport is maybe a fifth of the work: message history and pagination, delivery and read states, media uploads, typing indicators, group membership, moderation, and push routing all sit on top. That stack is why chat SDKs exist as a category.
Disclosure: we build one. Ethora is an open source chat and AI platform with a React Native SDK, where the transport, reconnection, and gap-fill logic in this article is already implemented on top of XMPP, and you can self-host the server. If your requirement is “real time chat in our app” rather than “a custom real time protocol”, compare the options in our chat SDKs for React Native guide before hand-rolling. If your requirement really is custom (live cursors, game state, telemetry), the patterns above are the way.
FAQ
Does WebSocket work in Expo?
Yes. The WebSocket API is part of React Native core and works in Expo Go and dev builds with no config.
Why does my socket keep disconnecting on Android?
Usually battery optimization (Doze) suspending the app, or a carrier reaping idle connections. Heartbeats plus reconnect-on-foreground handle the common cases; do not expect a background socket to stay alive.
WebSockets or Server-Sent Events?
SSE is simpler for one-way streams but has weaker support in React Native (no native EventSource; you need a polyfill). For bidirectional traffic or anything chat-shaped, WebSockets are the default answer.