Skip to main content

Building Your First WebSocket in React

A WebSocket connection in React starts with the native WebSocket API: create a new WebSocket(url), handle onopen, onmessage, and onerror events, and send messages with ws.send(). The key is wrapping this lifecycle in a useEffect hook and cleaning up the connection when the component unmounts. Below, you'll see a complete example with state management and error handling.

Creating Your First WebSocket Connection

The browser provides the native WebSocket constructor. Instantiate it with a WebSocket URL (starting with ws:// or wss:// for secure):

import { useEffect, useState } from 'react';

export function SimpleChat() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [status, setStatus] = useState('connecting');

useEffect(() => {
// Create a WebSocket connection
const ws = new WebSocket('wss://echo.websocket.org');

ws.addEventListener('open', () => {
console.log('Connected to server');
setStatus('connected');
});

ws.addEventListener('message', (event) => {
// Receive a message from the server
const msg = JSON.parse(event.data);
setMessages((prev) => [...prev, msg]);
});

ws.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
setStatus('error');
});

ws.addEventListener('close', () => {
console.log('Disconnected from server');
setStatus('disconnected');
});

// Cleanup: close the connection when component unmounts
return () => {
ws.close();
};
}, []);

const handleSend = () => {
// Send a message to the server
const payload = JSON.stringify({ text: input, timestamp: Date.now() });
// Note: In this example we're using an echo server, so the server
// echoes the message back to us. Real apps would broadcast to other clients.
const ws = new WebSocket('wss://echo.websocket.org');
ws.onopen = () => ws.send(payload);
setInput('');
};

return (
`<div>`
`<p>`Status: {status}`</p>`
`<div>`
{messages.map((msg, idx) => (
`<div key={idx}>`{msg.text} at {new Date(msg.timestamp).toLocaleTimeString()}`</div>`
))}
`</div>`
`<input`
value={input}
onChange={(e) => setInput(e.target.value)}
placeholder="Type a message"
`/>`
`<button onClick={handleSend}>`Send`</button>`
`</div>`
);
}

Key points:

  • new WebSocket(url) creates the connection asynchronously.
  • onopen: Fires when the server accepts the connection.
  • onmessage: Fires when the server sends a message.
  • onerror and onclose: Handle disconnections and errors.
  • The cleanup function (return () => ws.close()) closes the connection when the component unmounts, preventing memory leaks.

Managing Multiple Messages and State

Real apps receive many messages. Store them in state and render a list:

import { useEffect, useState, useRef } from 'react';

export function AdvancedChat() {
const [messages, setMessages] = useState([]);
const [input, setInput] = useState('');
const [connected, setConnected] = useState(false);
const wsRef = useRef(null);

useEffect(() => {
const ws = new WebSocket('wss://your-server.example.com/chat');

ws.addEventListener('open', () => {
setConnected(true);
// Optionally send a "joined" message to the server
ws.send(JSON.stringify({ type: 'joined', username: 'User123' }));
});

ws.addEventListener('message', (event) => {
try {
const msg = JSON.parse(event.data);
setMessages((prev) => [...prev, msg]);
} catch (error) {
console.error('Failed to parse message:', error);
}
});

ws.addEventListener('error', (error) => {
console.error('WebSocket error:', error);
});

ws.addEventListener('close', () => {
setConnected(false);
});

wsRef.current = ws;

return () => {
ws.close();
};
}, []);

const handleSend = () => {
if (!wsRef.current || wsRef.current.readyState !== WebSocket.OPEN) {
console.error('WebSocket is not connected');
return;
}

const payload = JSON.stringify({
type: 'message',
text: input,
timestamp: Date.now(),
username: 'User123',
});

wsRef.current.send(payload);
setInput('');
};

return (
`<div>`
`<h2>`Chat Room`</h2>`
`<p>`{connected ? 'Connected' : 'Disconnected'}`</p>`
`<div style={{ maxHeight: '400px', overflowY: 'auto' }}>`
{messages.map((msg, idx) => (
`<div key={idx}>`
`<strong>`{msg.username}:`</strong>` {msg.text}
` (`{new Date(msg.timestamp).toLocaleTimeString()}`)`
`</div>`
))}
`</div>`
`<input`
value={input}
onChange={(e) => setInput(e.target.value)}
disabled={!connected}
placeholder="Type a message"
`/>`
`<button onClick={handleSend} disabled={!connected}>`
Send
`</button>`
`</div>`
);
}

Notice the wsRef to store the WebSocket instance outside render cycles, allowing handleSend to access it. Check readyState before sending:

  • 0 = CONNECTING
  • 1 = OPEN
  • 2 = CLOSING
  • 3 = CLOSED

Server Implementation (Node.js with ws library)

Here's a minimal WebSocket server using the popular ws library:

import WebSocket from 'ws';
import http from 'http';

const server = http.createServer();
const wss = new WebSocket.Server({ server });

wss.on('connection', (ws) => {
console.log('Client connected');

ws.on('message', (data) => {
try {
const msg = JSON.parse(data);
console.log('Received:', msg);

// Broadcast to all connected clients
wss.clients.forEach((client) => {
if (client.readyState === WebSocket.OPEN) {
client.send(JSON.stringify(msg));
}
});
} catch (error) {
console.error('Parse error:', error);
}
});

ws.on('error', (error) => {
console.error('WebSocket error:', error);
});

ws.on('close', () => {
console.log('Client disconnected');
});
});

server.listen(3000, () => {
console.log('WebSocket server running on ws://localhost:3000');
});

This server broadcasts every message to all connected clients. In production, you'd add room/channel logic, authentication, and message validation.

Common Pitfalls and How to Avoid Them

Pitfall 1: Creating a new connection on every render. Always wrap new WebSocket() in useEffect with an empty dependency array. Otherwise, your component creates dozens of connections and memory leaks.

Pitfall 2: Not checking connection state before sending. Check ws.readyState === WebSocket.OPEN before calling ws.send(). Sending on a closed connection throws an error.

Pitfall 3: Ignoring cleanup. Always return () => ws.close() from useEffect. If you don't, the WebSocket stays open even after the component unmounts.

Pitfall 4: No error handling on message parse. Always wrap JSON.parse() in a try-catch. Malformed messages from the server will crash your component.

Key Takeaways

  • Use the native WebSocket API to create bidirectional, persistent connections.
  • Wrap the WebSocket lifecycle in useEffect with an empty dependency array.
  • Store the WebSocket instance in a ref (useRef) so event handlers can access it.
  • Always check readyState before sending and parse JSON safely with try-catch.
  • Implement cleanup in useEffect return to close the connection and prevent memory leaks.

Frequently Asked Questions

Can I use WebSocket with HTTPS?

Yes, use wss:// (WebSocket Secure) instead of ws://. Just like https encrypts HTTP, wss encrypts WebSocket traffic.

How do I reconnect if the connection drops?

Don't reconnect in this basic example. The next article covers reconnection strategies with exponential backoff, which are essential for production.

Should I use addEventListener or onmessage?

Both work identically for a single listener. addEventListener is slightly more flexible if you need multiple listeners for the same event.

What's the maximum message size?

Most browsers and servers allow messages up to 2 GB, but practical limits depend on your server's configuration. For large data, split it into chunks or use WebSocket subprotocols.

Further Reading