How to use WebSockets in React
WebSockets enable real-time bidirectional communication between your React app and the server — perfect for chat applications, live dashboards, notifications, and collaborative features. As the creator of CoreUI with 25 years of front-end development experience, I’ve built real-time dashboards that display live data streams from WebSocket servers, and the key is encapsulating all WebSocket logic in a custom hook. The hook manages connection lifecycle, reconnection logic, and cleanup so your components stay simple. Without cleanup, WebSocket connections outlive the component and continue consuming resources.
Create a custom hook that manages a WebSocket connection.
// useWebSocket.js
import { useState, useEffect, useRef, useCallback } from 'react'
export function useWebSocket(url) {
const [messages, setMessages] = useState([])
const [status, setStatus] = useState('connecting')
const wsRef = useRef(null)
useEffect(() => {
const ws = new WebSocket(url)
wsRef.current = ws
ws.onopen = () => setStatus('connected')
ws.onclose = () => setStatus('disconnected')
ws.onerror = () => setStatus('error')
ws.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
setMessages(prev => [...prev, data])
} catch {
setMessages(prev => [...prev, event.data])
}
}
// Cleanup: close connection when component unmounts
return () => {
ws.close()
}
}, [url])
const send = useCallback((data) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(typeof data === 'string' ? data : JSON.stringify(data))
}
}, [])
return { messages, status, send }
}
The useEffect cleanup function calls ws.close() when the component unmounts, preventing memory leaks. useRef holds the WebSocket instance so send can access it without being in the effect’s dependency array. useCallback ensures send is stable and doesn’t cause re-renders.
Using the Hook in a Chat Component
Connect to a WebSocket server and display messages.
// Chat.jsx
import { useState } from 'react'
import { useWebSocket } from './useWebSocket'
export function Chat() {
const { messages, status, send } = useWebSocket('wss://api.example.com/chat')
const [input, setInput] = useState('')
function handleSubmit(e) {
e.preventDefault()
if (!input.trim()) return
send({ type: 'message', text: input, timestamp: Date.now() })
setInput('')
}
return (
<div>
<p>Status: <strong>{status}</strong></p>
<div className="messages">
{messages.map((msg, i) => (
<div key={i} className="message">
{typeof msg === 'object' ? msg.text : msg}
</div>
))}
</div>
<form onSubmit={handleSubmit}>
<input
value={input}
onChange={e => setInput(e.target.value)}
disabled={status !== 'connected'}
placeholder="Type a message..."
/>
<button type="submit" disabled={status !== 'connected'}>Send</button>
</form>
</div>
)
}
Disabling the input and button when not connected prevents sending messages to a closed socket. The status indicator helps users understand why their messages aren’t going through.
Auto-Reconnect on Disconnect
Automatically reconnect when the connection drops.
// useWebSocket.js (with reconnection)
import { useState, useEffect, useRef, useCallback } from 'react'
export function useWebSocket(url, { reconnectDelay = 3000 } = {}) {
const [status, setStatus] = useState('connecting')
const [lastMessage, setLastMessage] = useState(null)
const wsRef = useRef(null)
const reconnectTimer = useRef(null)
const connect = useCallback(() => {
const ws = new WebSocket(url)
wsRef.current = ws
ws.onopen = () => setStatus('connected')
ws.onmessage = (event) => setLastMessage(event.data)
ws.onclose = () => {
setStatus('disconnected')
reconnectTimer.current = setTimeout(connect, reconnectDelay)
}
ws.onerror = () => ws.close()
}, [url, reconnectDelay])
useEffect(() => {
connect()
return () => {
clearTimeout(reconnectTimer.current)
wsRef.current?.close()
}
}, [connect])
const send = useCallback((data) => {
if (wsRef.current?.readyState === WebSocket.OPEN) {
wsRef.current.send(typeof data === 'string' ? data : JSON.stringify(data))
}
}, [])
return { status, lastMessage, send }
}
When the connection closes, onclose sets a timer to call connect again after the reconnect delay. The cleanup function clears the timer to prevent reconnect attempts after unmounting.
Best Practice Note
This is the same WebSocket hook pattern used in CoreUI real-time dashboard templates. For production, consider using socket.io-client instead of the raw WebSocket API — it adds automatic reconnection, namespaces, and rooms out of the box. For server-to-client only updates (no need to send from client), consider Server-Sent Events (SSE) which are simpler and work over standard HTTP. For complex real-time state management, pair WebSockets with Zustand or Redux to distribute incoming messages across the component tree.



