React Hooks WebSocket Integration: A Complete Guide with Examples
The proper way to use React Hooks with WebSockets is to store the socket instance in a useRef to maintain a stable reference across renders, manage the connection lifecycle inside a useEffect hook with proper cleanup, and expose the functionality through a custom useWebSocket hook that returns the send function and connection state.
React Hooks provide a declarative API for managing WebSocket connections in functional components. This guide demonstrates the canonical pattern used in the facebook/react repository, combining useRef, useEffect, and useState to handle real-time data streams without memory leaks or stale closures. The approach aligns with React's functional paradigm by treating the WebSocket as a side effect that synchronizes with your component's lifecycle.
The Core Pattern for React Hooks WebSocket Integration
Three built-in hooks work together to manage WebSocket connections safely. This pattern prevents common issues like duplicate connections, memory leaks, and stale closures.
Storing the Socket Instance with useRef
useRef holds a mutable reference to the WebSocket instance that survives re-renders without triggering them. Unlike useState, updating a ref does not cause the component to re-render, which is essential because the WebSocket object itself should remain stable throughout the component's lifetime.
In packages/react/src/ReactHooks.js, the core hook implementation demonstrates how React maintains persistent references across renders. Apply this same principle to your socket instance:
const socketRef = useRef<WebSocket | null>(null);
Managing Lifecycle with useEffect
useEffect handles the imperative WebSocket operations—opening connections, registering event listeners, and cleaning up. The effect runs after the DOM paints, which is the correct moment to establish network connections. Always return a cleanup function that closes the socket and removes listeners when the component unmounts or when dependencies change.
According to the React source patterns seen in fixtures/flight/src/index.js, the cleanup phase is critical for preventing memory leaks in long-running applications.
Tracking State with useState
useState stores incoming messages and connection status (readyState) so the UI can react to them. When the socket receives data, the message event listener updates state, triggering a re-render that displays the new data.
Building a Custom useWebSocket Hook
Encapsulating the logic in a custom hook provides a reusable API across your application. This implementation follows the exact pattern found in the React repository's handling of streaming connections.
import { useEffect, useRef, useState, useCallback } from 'react';
type Handlers = {
onMessage?: (event: MessageEvent) => void;
onOpen?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
onError?: (event: Event) => void;
};
export function useWebSocket(
url: string,
handlers: Handlers = {}
) {
const socketRef = useRef<WebSocket | null>(null);
const [readyState, setReadyState] = useState<number>(WebSocket.CLOSED);
const [lastMessage, setLastMessage] = useState<MessageEvent | null>(null);
useEffect(() => {
const ws = new WebSocket(url);
socketRef.current = ws;
setReadyState(ws.readyState);
const onOpen = (e: Event) => {
setReadyState(ws.readyState);
handlers.onOpen?.(e);
};
const onMessage = (e: MessageEvent) => {
setLastMessage(e);
handlers.onMessage?.(e);
};
const onClose = (e: CloseEvent) => {
setReadyState(ws.readyState);
handlers.onClose?.(e);
};
const onError = (e: Event) => {
handlers.onError?.(e);
};
ws.addEventListener('open', onOpen);
ws.addEventListener('message', onMessage);
ws.addEventListener('close', onClose);
ws.addEventListener('error', onError);
return () => {
ws.removeEventListener('open', onOpen);
ws.removeEventListener('message', onMessage);
ws.removeEventListener('close', onClose);
ws.removeEventListener('error', onError);
ws.close();
};
}, [url, handlers]);
const send = useCallback(
(data: string | ArrayBufferLike | Blob | ArrayBufferView) => {
if (socketRef.current?.readyState === WebSocket.OPEN) {
socketRef.current.send(data);
}
},
[]
);
return { send, lastMessage, readyState, socket: socketRef.current };
}
The hook creates the socket when the URL changes, wires up event listeners, and provides a send method wrapped in useCallback to maintain reference stability.
Implementing the Chat Component
Consume the custom hook in any functional component to access real-time messaging capabilities. The component remains declarative, describing what it wants (messages, send capability) while the hook manages how the socket operates.
import { useState } from 'react';
import { useWebSocket } from '../hooks/useWebSocket';
export function ChatRoom({ serverUrl }: { serverUrl: string }) {
const { send, lastMessage, readyState } = useWebSocket(serverUrl, {
onOpen: () => console.log('Connected'),
onClose: () => console.log('Disconnected'),
onError: err => console.error('WS error', err),
});
const [input, setInput] = useState('');
const handleSend = () => {
send(input);
setInput('');
};
return (
<div>
<h3>WebSocket Chat (status: {readyState})</h3>
{lastMessage && <p>Server says: {lastMessage.data}</p>}
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Message"
/>
<button onClick={handleSend} disabled={readyState !== WebSocket.OPEN}>
Send
</button>
</div>
);
}
The readyState value maps directly to the WebSocket specification constants: 0 (CONNECTING), 1 (OPEN), 2 (CLOSING), and 3 (CLOSED).
Real-World Examples in the React Codebase
The facebook/react repository demonstrates WebSocket integration in several key locations that validate this architectural approach.
Flight Fixtures and Server Components
The createWebSocketStream function in fixtures/flight/src/index.js demonstrates opening a WebSocket, wiring up ReadableStream and WritableStream interfaces, and handling lifecycle events for React Server Components debugging. This file shows the same principles in action: open the connection after setup, maintain a reference, and clean up on unmount.
React DevTools Backend
In packages/react-devtools-core/src/backend.js, the DevTools uses a WebSocket to communicate with the browser extension. This implementation demonstrates socket lifecycle management inside a library context, showing how to handle reconnection logic and error boundaries when the bridge disconnects unexpectedly.
Streaming Server Rendering
The file packages/react-server-dom-webpack/src/client/ReactFlightDOMClientBrowser.js handles WebSocket-based streaming of server-rendered data. Studying this implementation reveals how React consumes binary frames from a socket and converts them into component trees, useful for understanding high-performance data streaming patterns.
Complete Working Example
Below is a complete, runnable implementation you can integrate into a Create React App or Vite project.
import { useEffect, useRef, useState, useCallback } from 'react';
type Handlers = {
onMessage?: (event: MessageEvent) => void;
onOpen?: (event: Event) => void;
onClose?: (event: CloseEvent) => void;
onError?: (event: Event) => void;
};
export function useWebSocket(
url: string,
handlers: Handlers = {}
) {
const socketRef = useRef<WebSocket | null>(null);
const [readyState, setReadyState] = useState<number>(WebSocket.CLOSED);
const [lastMessage, setLastMessage] = useState<MessageEvent | null>(null);
useEffect(() => {
const ws = new WebSocket(url);
socketRef.current = ws;
setReadyState(ws.readyState);
const onOpen = (e: Event) => {
setReadyState(ws.readyState);
handlers.onOpen?.(e);
};
const onMessage = (e: MessageEvent) => {
setLastMessage(e);
handlers.onMessage?.(e);
};
const onClose = (e: CloseEvent) => {
setReadyState(ws.readyState);
handlers.onClose?.(e);
};
const onError = (e: Event) => {
handlers.onError?.(e);
};
ws.addEventListener('open', onOpen);
ws.addEventListener('message', onMessage);
ws.addEventListener('close', onClose);
ws.addEventListener('error', onError);
return () => {
ws.removeEventListener('open', onOpen);
ws.removeEventListener('message', onMessage);
ws.removeEventListener('close', onClose);
ws.removeEventListener('error', onError);
ws.close();
};
}, [url, handlers]);
const send = useCallback(
(data: string | ArrayBufferLike | Blob | ArrayBufferView) => {
if (socketRef.current?.readyState === WebSocket.OPEN) {
socketRef.current.send(data);
}
},
[]
);
return { send, lastMessage, readyState, socket: socketRef.current };
}
import { useState } from 'react';
import { useWebSocket } from '../hooks/useWebSocket';
export function ChatRoom({ serverUrl }: { serverUrl: string }) {
const { send, lastMessage, readyState } = useWebSocket(serverUrl, {
onOpen: () => console.log('Connected'),
onClose: () => console.log('Disconnected'),
onError: err => console.error('WS error', err),
});
const [input, setInput] = useState('');
const handleSend = () => {
send(input);
setInput('');
};
return (
<div>
<h3>WebSocket Chat (status: {readyState})</h3>
{lastMessage && <p>Server says: {lastMessage.data}</p>}
<input
value={input}
onChange={e => setInput(e.target.value)}
placeholder="Message"
/>
<button onClick={handleSend} disabled={readyState !== WebSocket.OPEN}>
Send
</button>
</div>
);
}
import { ChatRoom } from './components/ChatRoom';
function App() {
return (
<ChatRoom serverUrl="wss://your-websocket-endpoint.example.com" />
);
}
export default App;
Summary
- Use
useRefto maintain a stable WebSocket instance that persists across renders without causing re-renders. - Manage lifecycle in
useEffectto open connections after DOM paint and close them during cleanup to prevent memory leaks. - Track UI state with
useStateforreadyStateandlastMessageto enable reactive updates when data arrives. - Encapsulate logic in a custom hook (
useWebSocket) to create a reusable, testable API that can be shared across components. - Reference
fixtures/flight/src/index.jsin the React repository for a production example of WebSocket streaming with Server Components.
Frequently Asked Questions
Should I use useRef or useState to store the WebSocket instance?
Use useRef to store the WebSocket instance. Storing the socket in useState would trigger unnecessary re-renders every time you update the reference, and could lead to closing and reopening connections during render cycles. The useRef hook maintains a mutable reference that survives re-renders without notifying React, which is exactly what you need for an imperative API like WebSocket.
How do I prevent WebSocket reconnections on every render?
Place the WebSocket constructor inside a useEffect hook, not at the top level of your component or inside event handlers without memoization. The dependency array of the effect should only include values that actually require a new connection, such as the URL. Additionally, use useCallback for the send function to ensure stable references when passed to child components.
Can I use multiple WebSocket connections in a single component?
Yes, you can call your custom useWebSocket hook multiple times with different URLs, or instantiate multiple refs and effects manually. Each connection maintains its own independent lifecycle. However, consider whether multiple connections are necessary for your use case, as browsers limit the total number of simultaneous WebSocket connections per domain.
How do I handle automatic reconnection with React Hooks?
Implement reconnection logic inside the onClose handler of your custom hook. Use a retry counter stored in a ref (to avoid re-render loops) and setTimeout to delay reconnection attempts. Update the useEffect dependency array to include a retryAttempt state that increments when a connection drops, triggering the effect to create a new WebSocket instance. The packages/react-devtools-core/src/backend.js file in the React repository demonstrates robust reconnection patterns for persistent tooling connections.
Have a question about this repo?
These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:
curl -s https://instagit.com/install.md