
Introduction
Real-time features are now expected in modern mobile apps—whether it’s live chat, push notifications, stock tickers, multiplayer games, or collaborative editing. Users expect instant updates without refreshing or polling. WebSockets provide the foundation for this real-time communication, offering full-duplex connections that let servers push data to clients instantly. Apps like Slack, Discord, and Robinhood rely heavily on WebSocket connections to deliver their real-time experiences. In this comprehensive guide, you’ll learn how to integrate WebSocket real-time features in React Native, including connection management with custom hooks, state management patterns, reconnection strategies, authentication handling, and production-ready patterns for building reliable real-time applications.
Understanding WebSockets
WebSocket is a protocol that provides full-duplex communication over a single persistent TCP connection. Unlike HTTP’s request-response model, WebSockets allow both client and server to send messages at any time without the overhead of establishing new connections.
Key advantages over polling:
Lower latency: Messages arrive instantly without waiting for the next poll interval.
Reduced bandwidth: No repeated HTTP headers or connection overhead.
Server-initiated messages: The server can push updates without client requests.
Battery efficiency: Fewer network requests means better battery life on mobile.
Basic WebSocket Setup
React Native supports WebSockets natively using the browser-style API:
// Basic WebSocket connection
const socket = new WebSocket('wss://api.example.com/ws');
socket.onopen = () => {
console.log('Connected to WebSocket');
socket.send(JSON.stringify({ type: 'subscribe', channel: 'updates' }));
};
socket.onmessage = (event) => {
const data = JSON.parse(event.data);
console.log('Received:', data);
};
socket.onerror = (error) => {
console.error('WebSocket error:', error.message);
};
socket.onclose = (event) => {
console.log(`WebSocket closed: ${event.code} ${event.reason}`);
};
Production-Ready WebSocket Hook
For real applications, wrap WebSocket logic in a custom hook with reconnection and state management:
// src/hooks/useWebSocket.ts
import { useEffect, useRef, useState, useCallback } from 'react';
interface WebSocketOptions {
url: string;
onMessage?: (data: any) => void;
onConnect?: () => void;
onDisconnect?: () => void;
onError?: (error: Event) => void;
reconnect?: boolean;
reconnectInterval?: number;
maxReconnectAttempts?: number;
}
interface WebSocketState {
isConnected: boolean;
isConnecting: boolean;
reconnectAttempts: number;
}
export function useWebSocket(options: WebSocketOptions) {
const {
url,
onMessage,
onConnect,
onDisconnect,
onError,
reconnect = true,
reconnectInterval = 3000,
maxReconnectAttempts = 5,
} = options;
const socketRef = useRef(null);
const reconnectTimeoutRef = useRef();
const mountedRef = useRef(true);
const [state, setState] = useState({
isConnected: false,
isConnecting: false,
reconnectAttempts: 0,
});
const connect = useCallback(() => {
if (!mountedRef.current) return;
if (socketRef.current?.readyState === WebSocket.OPEN) return;
setState(prev => ({ ...prev, isConnecting: true }));
const socket = new WebSocket(url);
socketRef.current = socket;
socket.onopen = () => {
if (!mountedRef.current) return;
setState({
isConnected: true,
isConnecting: false,
reconnectAttempts: 0,
});
onConnect?.();
};
socket.onmessage = (event) => {
if (!mountedRef.current) return;
try {
const data = JSON.parse(event.data);
onMessage?.(data);
} catch (error) {
console.error('Failed to parse WebSocket message:', error);
}
};
socket.onerror = (error) => {
if (!mountedRef.current) return;
console.error('WebSocket error:', error);
onError?.(error);
};
socket.onclose = (event) => {
if (!mountedRef.current) return;
setState(prev => ({
...prev,
isConnected: false,
isConnecting: false,
}));
onDisconnect?.();
// Attempt reconnection if enabled and not a clean close
if (reconnect && event.code !== 1000) {
setState(prev => {
if (prev.reconnectAttempts < maxReconnectAttempts) {
const delay = reconnectInterval * Math.pow(2, prev.reconnectAttempts);
console.log(`Reconnecting in ${delay}ms (attempt ${prev.reconnectAttempts + 1})`);
reconnectTimeoutRef.current = setTimeout(() => {
connect();
}, delay);
return { ...prev, reconnectAttempts: prev.reconnectAttempts + 1 };
}
console.error('Max reconnection attempts reached');
return prev;
});
}
};
}, [url, onMessage, onConnect, onDisconnect, onError, reconnect, reconnectInterval, maxReconnectAttempts]);
const disconnect = useCallback(() => {
if (reconnectTimeoutRef.current) {
clearTimeout(reconnectTimeoutRef.current);
}
if (socketRef.current) {
socketRef.current.close(1000, 'Client disconnect');
socketRef.current = null;
}
}, []);
const send = useCallback((data: object) => {
if (socketRef.current?.readyState === WebSocket.OPEN) {
socketRef.current.send(JSON.stringify(data));
return true;
}
console.warn('WebSocket not connected, message not sent');
return false;
}, []);
useEffect(() => {
mountedRef.current = true;
connect();
return () => {
mountedRef.current = false;
disconnect();
};
}, [connect, disconnect]);
return {
...state,
send,
disconnect,
reconnect: connect,
};
}
WebSocket Context for App-Wide Access
Share the WebSocket connection across your app with React Context:
// src/context/WebSocketContext.tsx
import React, { createContext, useContext, useCallback, useState, ReactNode } from 'react';
import { useWebSocket } from '../hooks/useWebSocket';
interface Message {
id: string;
type: string;
payload: any;
timestamp: number;
}
interface WebSocketContextValue {
isConnected: boolean;
isConnecting: boolean;
send: (data: object) => boolean;
subscribe: (type: string, handler: (payload: any) => void) => () => void;
lastMessage: Message | null;
}
const WebSocketContext = createContext(null);
interface WebSocketProviderProps {
url: string;
children: ReactNode;
}
export function WebSocketProvider({ url, children }: WebSocketProviderProps) {
const [lastMessage, setLastMessage] = useState(null);
const [handlers] = useState
Building a Real-Time Chat Feature
// src/features/chat/screens/ChatScreen.tsx
import React, { useState, useEffect, useCallback } from 'react';
import { View, FlatList, TextInput, StyleSheet, KeyboardAvoidingView, Platform } from 'react-native';
import { useWebSocketContext } from '@/context/WebSocketContext';
import { ChatMessage } from '../components/ChatMessage';
import { Button } from '@/components/ui';
interface Message {
id: string;
userId: string;
userName: string;
text: string;
timestamp: number;
}
export function ChatScreen({ roomId }: { roomId: string }) {
const [messages, setMessages] = useState([]);
const [inputText, setInputText] = useState('');
const { isConnected, send, subscribe } = useWebSocketContext();
useEffect(() => {
// Join the room
send({ type: 'room:join', payload: { roomId } });
// Subscribe to new messages
const unsubscribeMessage = subscribe('chat:message', (payload: Message) => {
setMessages(prev => [...prev, payload]);
});
// Subscribe to message history
const unsubscribeHistory = subscribe('chat:history', (payload: Message[]) => {
setMessages(payload);
});
// Subscribe to user events
const unsubscribeUserJoined = subscribe('room:user_joined', (payload) => {
console.log(`${payload.userName} joined the room`);
});
return () => {
send({ type: 'room:leave', payload: { roomId } });
unsubscribeMessage();
unsubscribeHistory();
unsubscribeUserJoined();
};
}, [roomId, send, subscribe]);
const handleSend = useCallback(() => {
if (!inputText.trim() || !isConnected) return;
send({
type: 'chat:send',
payload: {
roomId,
text: inputText.trim(),
},
});
setInputText('');
}, [inputText, isConnected, roomId, send]);
return (
item.id}
renderItem={({ item }) => }
inverted={false}
contentContainerStyle={styles.messageList}
/>
);
}
const styles = StyleSheet.create({
container: { flex: 1 },
messageList: { padding: 16 },
inputContainer: {
flexDirection: 'row',
padding: 16,
borderTopWidth: 1,
borderTopColor: '#e0e0e0',
gap: 8,
},
input: {
flex: 1,
borderWidth: 1,
borderColor: '#e0e0e0',
borderRadius: 8,
paddingHorizontal: 12,
paddingVertical: 8,
},
});
WebSocket Authentication
Authenticate WebSocket connections securely:
// Method 1: Token in URL query parameter
const token = await getAuthToken();
const socket = new WebSocket(`wss://api.example.com/ws?token=${token}`);
// Method 2: Send auth message after connection
const socket = new WebSocket('wss://api.example.com/ws');
socket.onopen = async () => {
const token = await getAuthToken();
socket.send(JSON.stringify({
type: 'auth',
payload: { token },
}));
};
// Method 3: Handle auth in your hook
export function useAuthenticatedWebSocket(options: WebSocketOptions) {
const [isAuthenticated, setIsAuthenticated] = useState(false);
const handleMessage = useCallback((data: any) => {
if (data.type === 'auth:success') {
setIsAuthenticated(true);
} else if (data.type === 'auth:error') {
console.error('Authentication failed:', data.payload.message);
} else if (isAuthenticated) {
options.onMessage?.(data);
}
}, [isAuthenticated, options]);
const ws = useWebSocket({
...options,
onMessage: handleMessage,
onConnect: async () => {
const token = await getAuthToken();
ws.send({ type: 'auth', payload: { token } });
},
});
return { ...ws, isAuthenticated };
}
Common Mistakes to Avoid
Not handling reconnection: Mobile networks are unreliable. Always implement exponential backoff reconnection logic.
Memory leaks: Clean up WebSocket connections and event handlers in useEffect cleanup functions.
Missing heartbeats: Some proxies close idle connections. Implement ping/pong messages to keep connections alive.
Blocking the main thread: Process large messages asynchronously to avoid UI freezes.
Ignoring connection state: Always check isConnected before sending messages and show appropriate UI states.
Conclusion
WebSockets bring low-latency, real-time communication to React Native with relatively simple setup. The key to production-ready implementations is proper lifecycle management, reconnection logic with exponential backoff, and clean state management through hooks and context. Whether you’re building chat, live updates, or collaborative features, these patterns provide a solid foundation. Always implement authentication, handle connection drops gracefully, and show users clear feedback about connection status. For backend implementation patterns, check out our guide on Scalable Express.js Project Structure. For the WebSocket protocol specification, explore the MDN WebSocket documentation.