React WebSocket Guide
This guide covers everything you need to know about implementing React WebSocket connections in your applications. Whether you’re building a real-time chat, live notifications, or collaborative features, understanding how to properly integrate websocket in react applications is critical for creating responsive, bidirectional communication between your client and server.
We’ll explore multiple approaches: using the native WebSocket API with React hooks, building a custom useWebSocket hook, integrating third-party libraries like react-use-websocket, managing state with various patterns including react query websocket integration, handling reconnection logic, and implementing react native websocket connections for mobile apps.
Understanding WebSocket in React
React’s component lifecycle and state management create unique challenges when working with WebSocket connections. Unlike traditional HTTP requests, WebSocket connections are persistent, bidirectional channels that need careful handling to avoid memory leaks, duplicate connections, and state synchronization issues.
The key challenges you’ll face include:
- Establishing connections at the right time in the component lifecycle
- Cleaning up connections when components unmount
- Managing reconnection logic when connections drop
- Synchronizing WebSocket messages with React state
- Handling multiple components that need access to the same connection
Native WebSocket API with React Hooks
The most straightforward approach to implementing react js websocket functionality is using the native WebSocket API with useEffect and useRef hooks. This gives you full control over the connection lifecycle.
Here’s a basic implementation:
import { useEffect, useRef, useState } from 'react';
function ChatComponent() {
const [messages, setMessages] = useState([]);
const [connectionStatus, setConnectionStatus] = useState('Disconnected');
const ws = useRef(null);
useEffect(() => {
// Create WebSocket connection
ws.current = new WebSocket('wss://your-server.com/chat');
ws.current.onopen = () => {
console.log('WebSocket Connected');
setConnectionStatus('Connected');
};
ws.current.onmessage = (event) => {
const message = JSON.parse(event.data);
setMessages((prev) => [...prev, message]);
};
ws.current.onerror = (error) => {
console.error('WebSocket error:', error);
setConnectionStatus('Error');
};
ws.current.onclose = () => {
console.log('WebSocket Disconnected');
setConnectionStatus('Disconnected');
};
// Cleanup function
return () => {
if (ws.current) {
ws.current.close();
}
};
}, []); // Empty dependency array means this runs once on mount
const sendMessage = (text) => {
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify({ text, timestamp: Date.now() }));
}
};
return (
<div>
<div>Status: {connectionStatus}</div>
<div>
{messages.map((msg, index) => (
<div key={index}>{msg.text}</div>
))}
</div>
<button onClick={() => sendMessage('Hello')}>Send Message</button>
</div>
);
}
This implementation uses useRef to store the WebSocket instance because you don’t want React to re-render when the WebSocket object changes. The useEffect hook with an empty dependency array ensures the connection is established once when the component mounts and cleaned up when it unmounts.
Handling Component Re-renders
One common mistake is creating new WebSocket connections on every render. Always use useRef to persist the connection across renders:
// Wrong - creates new connection on every render
function BadExample() {
const ws = new WebSocket('wss://example.com');
// ...
}
// Correct - persists connection across renders
function GoodExample() {
const ws = useRef(null);
useEffect(() => {
ws.current = new WebSocket('wss://example.com');
return () => ws.current?.close();
}, []);
// ...
}
Building a Custom useWebSocket Hook
For reusable react websocket functionality, create a custom hook that encapsulates all WebSocket logic. This react usewebsocket pattern makes it easy to add WebSocket connections to any component.
import { useEffect, useRef, useState, useCallback } from 'react';
interface UseWebSocketOptions {
onOpen?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
onMessage?: (event: MessageEvent) => void;
onError?: (event: Event) => void;
reconnect?: boolean;
reconnectInterval?: number;
reconnectAttempts?: number;
}
function useWebSocket(url: string, options: UseWebSocketOptions = {}) {
const {
onOpen,
onClose,
onMessage,
onError,
reconnect = true,
reconnectInterval = 3000,
reconnectAttempts = 5
} = options;
const [readyState, setReadyState] = useState<number>(WebSocket.CONNECTING);
const [lastMessage, setLastMessage] = useState<MessageEvent | null>(null);
const ws = useRef<WebSocket | null>(null);
const reconnectCount = useRef(0);
const reconnectTimeout = useRef<NodeJS.Timeout>();
const connect = useCallback(() => {
try {
ws.current = new WebSocket(url);
ws.current.onopen = (event) => {
setReadyState(WebSocket.OPEN);
reconnectCount.current = 0;
onOpen?.(event);
};
ws.current.onclose = (event) => {
setReadyState(WebSocket.CLOSED);
onClose?.(event);
// Attempt reconnection
if (reconnect && reconnectCount.current < reconnectAttempts) {
reconnectCount.current++;
reconnectTimeout.current = setTimeout(() => {
console.log(`Reconnecting... Attempt ${reconnectCount.current}`);
connect();
}, reconnectInterval);
}
};
ws.current.onmessage = (event) => {
setLastMessage(event);
onMessage?.(event);
};
ws.current.onerror = (event) => {
setReadyState(WebSocket.CLOSED);
onError?.(event);
};
} catch (error) {
console.error('WebSocket connection error:', error);
}
}, [url, onOpen, onClose, onMessage, onError, reconnect, reconnectInterval, reconnectAttempts]);
useEffect(() => {
connect();
return () => {
if (reconnectTimeout.current) {
clearTimeout(reconnectTimeout.current);
}
ws.current?.close();
};
}, [connect]);
const sendMessage = useCallback((data: string | ArrayBufferLike | Blob | ArrayBufferView) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(data);
} else {
console.warn('WebSocket is not open. ReadyState:', ws.current?.readyState);
}
}, []);
const closeConnection = useCallback(() => {
reconnectCount.current = reconnectAttempts; // Prevent reconnection
ws.current?.close();
}, [reconnectAttempts]);
return {
sendMessage,
lastMessage,
readyState,
close: closeConnection,
getWebSocket: () => ws.current
};
}
export default useWebSocket;
Usage of this custom hook:
function LiveFeed() {
const { sendMessage, lastMessage, readyState } = useWebSocket(
'wss://api.example.com/live',
{
onOpen: () => console.log('Connected to live feed'),
onMessage: (event) => {
console.log('Received:', event.data);
},
reconnect: true,
reconnectAttempts: 10
}
);
const isConnected = readyState === WebSocket.OPEN;
return (
<div>
<div>Status: {isConnected ? 'Connected' : 'Disconnected'}</div>
<button
onClick={() => sendMessage('ping')}
disabled={!isConnected}
>
Send Ping
</button>
{lastMessage && <div>Last: {lastMessage.data}</div>}
</div>
);
}
Using the react-use-websocket Library
For production applications, the react-use-websocket library provides a battle-tested solution with advanced features. This library handles many edge cases and provides a clean API.
Install it:
npm install react-use-websocket
Basic react websocket example using the library:
import useWebSocket, { ReadyState } from 'react-use-websocket';
function NotificationCenter() {
const [notifications, setNotifications] = useState([]);
const { sendMessage, lastMessage, readyState } = useWebSocket(
'wss://api.example.com/notifications',
{
onOpen: () => console.log('WebSocket connection established'),
shouldReconnect: (closeEvent) => true,
reconnectAttempts: 10,
reconnectInterval: 3000,
}
);
useEffect(() => {
if (lastMessage !== null) {
const notification = JSON.parse(lastMessage.data);
setNotifications((prev) => [notification, ...prev]);
}
}, [lastMessage]);
const connectionStatus = {
[ReadyState.CONNECTING]: 'Connecting',
[ReadyState.OPEN]: 'Connected',
[ReadyState.CLOSING]: 'Closing',
[ReadyState.CLOSED]: 'Disconnected',
[ReadyState.UNINSTANTIATED]: 'Uninstantiated',
}[readyState];
return (
<div>
<header>
<h2>Notifications</h2>
<span className={`status ${readyState === ReadyState.OPEN ? 'active' : ''}`}>
{connectionStatus}
</span>
</header>
<div className="notifications">
{notifications.map((notif, idx) => (
<div key={idx} className="notification">
<strong>{notif.title}</strong>
<p>{notif.message}</p>
</div>
))}
</div>
</div>
);
}
The library also supports advanced features like filtering messages, sharing connections between components, and conditional connections:
function AdvancedUsage() {
const [shouldConnect, setShouldConnect] = useState(false);
const { sendJsonMessage, lastJsonMessage } = useWebSocket(
'wss://api.example.com/data',
{
share: true, // Share connection across components
filter: (message) => {
const data = JSON.parse(message.data);
return data.type === 'update'; // Only process 'update' messages
},
retryOnError: true,
shouldReconnect: (closeEvent) => {
// Custom reconnection logic based on close code
return closeEvent.code !== 1000;
},
},
shouldConnect // Conditional connection
);
return (
<div>
<button onClick={() => setShouldConnect(!shouldConnect)}>
{shouldConnect ? 'Disconnect' : 'Connect'}
</button>
<button onClick={() => sendJsonMessage({ action: 'subscribe', channel: 'updates' })}>
Subscribe
</button>
</div>
);
}
State Management Patterns
Managing state with react sockets requires careful consideration of how WebSocket messages update your application state. Here are several patterns for different state management solutions.
Local State with Context API
For medium-sized applications, combining WebSocket connections with React Context provides a clean way to share real-time data:
import { createContext, useContext, useEffect, useState } from 'react';
import useWebSocket from 'react-use-websocket';
const WebSocketContext = createContext(null);
export function WebSocketProvider({ children }) {
const [messages, setMessages] = useState([]);
const [users, setUsers] = useState([]);
const { sendJsonMessage, lastJsonMessage, readyState } = useWebSocket(
'wss://api.example.com/realtime',
{
shouldReconnect: () => true,
reconnectAttempts: 10,
}
);
useEffect(() => {
if (lastJsonMessage) {
switch (lastJsonMessage.type) {
case 'message':
setMessages((prev) => [...prev, lastJsonMessage.data]);
break;
case 'user_joined':
setUsers((prev) => [...prev, lastJsonMessage.data]);
break;
case 'user_left':
setUsers((prev) => prev.filter(u => u.id !== lastJsonMessage.data.id));
break;
}
}
}, [lastJsonMessage]);
const value = {
messages,
users,
sendMessage: sendJsonMessage,
isConnected: readyState === WebSocket.OPEN,
};
return (
<WebSocketContext.Provider value={value}>
{children}
</WebSocketContext.Provider>
);
}
export function useWebSocketContext() {
const context = useContext(WebSocketContext);
if (!context) {
throw new Error('useWebSocketContext must be used within WebSocketProvider');
}
return context;
}
Integration with React Query
While React Query is primarily designed for HTTP requests, you can integrate react query websocket functionality for cache invalidation and optimistic updates:
import { useQueryClient, useQuery } from '@tanstack/react-query';
import useWebSocket from 'react-use-websocket';
function useRealtimeData() {
const queryClient = useQueryClient();
// Initial data fetch
const { data: initialData } = useQuery({
queryKey: ['dashboard'],
queryFn: () => fetch('/api/dashboard').then(r => r.json()),
});
// WebSocket for real-time updates
const { lastJsonMessage } = useWebSocket('wss://api.example.com/updates', {
shouldReconnect: () => true,
});
useEffect(() => {
if (lastJsonMessage) {
// Invalidate and refetch
if (lastJsonMessage.action === 'invalidate') {
queryClient.invalidateQueries({ queryKey: ['dashboard'] });
}
// Optimistic update
if (lastJsonMessage.action === 'update') {
queryClient.setQueryData(['dashboard'], (old) => ({
...old,
...lastJsonMessage.data,
}));
}
}
}, [lastJsonMessage, queryClient]);
return initialData;
}
Zustand Integration
For global state management, Zustand works well with WebSocket connections:
import create from 'zustand';
import useWebSocket from 'react-use-websocket';
const useStore = create((set) => ({
messages: [],
connectionStatus: 'disconnected',
addMessage: (message) => set((state) => ({
messages: [...state.messages, message]
})),
setConnectionStatus: (status) => set({ connectionStatus: status }),
clearMessages: () => set({ messages: [] }),
}));
function useRealtimeStore() {
const addMessage = useStore((state) => state.addMessage);
const setConnectionStatus = useStore((state) => state.setConnectionStatus);
useWebSocket('wss://api.example.com/stream', {
onOpen: () => setConnectionStatus('connected'),
onClose: () => setConnectionStatus('disconnected'),
onMessage: (event) => {
const message = JSON.parse(event.data);
addMessage(message);
},
shouldReconnect: () => true,
});
}
function MessagesDisplay() {
useRealtimeStore(); // Initialize WebSocket
const messages = useStore((state) => state.messages);
const status = useStore((state) => state.connectionStatus);
return (
<div>
<div>Status: {status}</div>
{messages.map((msg, idx) => (
<div key={idx}>{msg.text}</div>
))}
</div>
);
}
Reconnection Strategies
Handling connection drops is critical for production react and websockets applications. You need strategies for automatic reconnection, exponential backoff, and user feedback.
Exponential Backoff
Implement exponential backoff to avoid overwhelming the server during outages:
function useWebSocketWithBackoff(url: string) {
const [reconnectDelay, setReconnectDelay] = useState(1000);
const maxDelay = 30000;
const ws = useRef<WebSocket | null>(null);
const connect = useCallback(() => {
ws.current = new WebSocket(url);
ws.current.onopen = () => {
console.log('Connected');
setReconnectDelay(1000); // Reset delay on successful connection
};
ws.current.onclose = (event) => {
// Check close code to determine if reconnection is appropriate
if (event.code !== 1000) {
const nextDelay = Math.min(reconnectDelay * 2, maxDelay);
console.log(`Reconnecting in ${nextDelay}ms...`);
setTimeout(() => {
setReconnectDelay(nextDelay);
connect();
}, reconnectDelay);
}
};
}, [url, reconnectDelay]);
useEffect(() => {
connect();
return () => ws.current?.close();
}, [connect]);
return ws;
}
Understanding WebSocket close codes helps you determine when reconnection is appropriate. For example, code 1000 indicates a normal closure where reconnection isn’t needed, while code 1006 indicates an abnormal closure that should trigger reconnection.
User-Triggered Reconnection
Give users control over reconnection:
function ConnectionManager() {
const [url, setUrl] = useState('wss://api.example.com/socket');
const [shouldConnect, setShouldConnect] = useState(true);
const { readyState, sendMessage } = useWebSocket(
url,
{
shouldReconnect: (closeEvent) => shouldConnect,
reconnectAttempts: 10,
reconnectInterval: (attemptNumber) =>
Math.min(Math.pow(2, attemptNumber) * 1000, 30000),
},
shouldConnect
);
const manualReconnect = () => {
setShouldConnect(false);
setTimeout(() => setShouldConnect(true), 100);
};
return (
<div>
<div>Status: {readyState === WebSocket.OPEN ? 'Connected' : 'Disconnected'}</div>
<button onClick={manualReconnect} disabled={readyState === WebSocket.OPEN}>
Reconnect
</button>
<button onClick={() => setShouldConnect(false)}>
Disconnect
</button>
</div>
);
}
React Native WebSocket
WebSocket support in react native websocket applications uses the same WebSocket API available in browsers. React Native includes WebSocket support out of the box.
Basic React Native implementation:
import React, { useEffect, useRef, useState } from 'react';
import { View, Text, Button, FlatList, StyleSheet } from 'react-native';
function ChatScreen() {
const [messages, setMessages] = useState([]);
const [isConnected, setIsConnected] = useState(false);
const ws = useRef(null);
useEffect(() => {
// React Native uses the same WebSocket API
ws.current = new WebSocket('wss://your-server.com/chat');
ws.current.onopen = () => {
console.log('WebSocket connected');
setIsConnected(true);
};
ws.current.onmessage = (e) => {
const message = JSON.parse(e.data);
setMessages((prev) => [...prev, message]);
};
ws.current.onerror = (e) => {
console.error('WebSocket error:', e.message);
};
ws.current.onclose = () => {
console.log('WebSocket disconnected');
setIsConnected(false);
};
return () => {
ws.current?.close();
};
}, []);
const sendMessage = (text) => {
if (ws.current && ws.current.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify({ text, timestamp: Date.now() }));
}
};
return (
<View style={styles.container}>
<Text style={styles.status}>
{isConnected ? 'Connected' : 'Disconnected'}
</Text>
<FlatList
data={messages}
keyExtractor={(item, index) => index.toString()}
renderItem={({ item }) => (
<View style={styles.message}>
<Text>{item.text}</Text>
</View>
)}
/>
<Button
title="Send Message"
onPress={() => sendMessage('Hello from React Native')}
disabled={!isConnected}
/>
</View>
);
}
const styles = StyleSheet.create({
container: {
flex: 1,
padding: 16,
},
status: {
fontSize: 16,
marginBottom: 12,
fontWeight: 'bold',
},
message: {
padding: 12,
backgroundColor: '#f0f0f0',
marginBottom: 8,
borderRadius: 8,
},
});
export default ChatScreen;
Expo WebSocket
When using expo websocket in Expo projects, the WebSocket API works identically. However, you need to handle platform-specific considerations:
import { useEffect, useRef, useState } from 'react';
import { Platform } from 'react-native';
function useExpoWebSocket(url) {
const ws = useRef(null);
const [readyState, setReadyState] = useState(WebSocket.CONNECTING);
useEffect(() => {
// Expo supports WebSocket on both iOS and Android
ws.current = new WebSocket(url);
ws.current.onopen = () => {
setReadyState(WebSocket.OPEN);
// Platform-specific logic if needed
if (Platform.OS === 'ios') {
console.log('WebSocket connected on iOS');
} else if (Platform.OS === 'android') {
console.log('WebSocket connected on Android');
}
};
ws.current.onclose = () => {
setReadyState(WebSocket.CLOSED);
};
return () => {
if (ws.current) {
ws.current.close();
}
};
}, [url]);
const send = (data) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(data);
}
};
return { send, readyState };
}
React Native Background Connections
Keep in mind that WebSocket connections in React Native will close when the app goes to background. For background updates, consider using push notifications instead:
import { AppState } from 'react-native';
function useBackgroundAwareWebSocket(url) {
const ws = useRef(null);
const appState = useRef(AppState.currentState);
useEffect(() => {
const subscription = AppState.addEventListener('change', (nextAppState) => {
if (appState.current.match(/inactive|background/) && nextAppState === 'active') {
// App came to foreground, reconnect
console.log('App foregrounded, reconnecting WebSocket');
if (ws.current?.readyState !== WebSocket.OPEN) {
ws.current = new WebSocket(url);
}
}
if (nextAppState.match(/inactive|background/)) {
// App going to background, close connection
console.log('App backgrounded, closing WebSocket');
ws.current?.close();
}
appState.current = nextAppState;
});
return () => {
subscription.remove();
};
}, [url]);
return ws;
}
Testing WebSocket Connections
Testing react websocket implementations requires mocking WebSocket connections. You can use libraries like mock-socket or create manual mocks.
Using mock-socket for testing:
import { Server } from 'mock-socket';
import { render, screen, waitFor } from '@testing-library/react';
import ChatComponent from './ChatComponent';
describe('ChatComponent', () => {
let mockServer;
beforeEach(() => {
mockServer = new Server('wss://your-server.com/chat');
});
afterEach(() => {
mockServer.close();
});
test('receives messages from WebSocket', async () => {
render(<ChatComponent />);
mockServer.on('connection', (socket) => {
socket.send(JSON.stringify({ text: 'Hello from server' }));
});
await waitFor(() => {
expect(screen.getByText('Hello from server')).toBeInTheDocument();
});
});
test('sends messages through WebSocket', async () => {
const { getByText } = render(<ChatComponent />);
const messageReceived = new Promise((resolve) => {
mockServer.on('connection', (socket) => {
socket.on('message', (data) => {
resolve(JSON.parse(data));
});
});
});
getByText('Send Message').click();
const message = await messageReceived;
expect(message.text).toBe('Hello');
});
});
For more comprehensive testing strategies, see our guide on how to test WebSockets.
Performance Optimization
When working with react usewebsocket patterns, optimize for performance by debouncing messages and avoiding unnecessary re-renders:
import { useEffect, useRef, useState, useCallback } from 'react';
import { debounce } from 'lodash';
function useOptimizedWebSocket(url) {
const [messages, setMessages] = useState([]);
const ws = useRef(null);
// Debounce message processing to avoid excessive state updates
const addMessage = useCallback(
debounce((message) => {
setMessages((prev) => [...prev, message]);
}, 100),
[]
);
useEffect(() => {
ws.current = new WebSocket(url);
ws.current.onmessage = (event) => {
const message = JSON.parse(event.data);
addMessage(message);
};
return () => {
addMessage.cancel(); // Cancel pending debounced calls
ws.current?.close();
};
}, [url, addMessage]);
// Batch send multiple messages
const sendBatch = useCallback((messagesArray) => {
if (ws.current?.readyState === WebSocket.OPEN) {
ws.current.send(JSON.stringify({ type: 'batch', messages: messagesArray }));
}
}, []);
return { messages, sendBatch };
}
Use React.memo to prevent child components from re-rendering when WebSocket state changes:
const MessageItem = React.memo(({ message }) => (
<div className="message">
<span>{message.user}</span>
<p>{message.text}</p>
</div>
));
function MessageList({ messages }) {
return (
<div>
{messages.map((msg) => (
<MessageItem key={msg.id} message={msg} />
))}
</div>
);
}
Frequently Asked Questions
How do I prevent multiple WebSocket connections in React?
Use useRef to store the WebSocket instance and create the connection inside useEffect with an empty dependency array. This ensures the connection is created only once when the component mounts. Always include a cleanup function that closes the connection when the component unmounts.
const ws = useRef(null);
useEffect(() => {
ws.current = new WebSocket('wss://example.com');
return () => ws.current?.close();
}, []); // Empty array = runs once
Should I use a library like react-use-websocket or build my own hook?
For production applications, use react-use-websocket. It handles edge cases like reconnection, message queuing, connection sharing, and browser compatibility. Build your own hook only if you have specific requirements that the library doesn’t support or if you’re building a learning project.
How do I share a WebSocket connection between multiple React components?
Use React Context to provide the WebSocket connection to multiple components. Create a context provider that manages the connection and exposes methods through context. Alternatively, use react-use-websocket with the share: true option, which automatically shares connections with the same URL across components.
Why does my WebSocket connection close when my React component re-renders?
This happens when you create the WebSocket connection outside of useEffect or in the component body. The connection needs to be created in useEffect with proper dependencies, and stored in a useRef to persist across re-renders. Make sure your cleanup function only runs when the component unmounts, not on every render.
Related Resources
- What is WebSocket - Learn WebSocket fundamentals
- JavaScript WebSocket Guide - Vanilla JavaScript implementation
- WebSocket Close Codes - Understanding connection closure
- How to Test WebSockets - Testing strategies and tools