Real-time features โ live chat, notifications, collaborative editing, live dashboards โ require a persistent connection. In React, Socket.io is the most practical way to add WebSocket-based real-time functionality. This guide shows how to integrate it cleanly with React hooks.
๐ Table of Contents
Setup
npm install socket.io-client
Basic Connection
// socket.js - create a single shared socket instance
import { io } from 'socket.io-client';
export const socket = io('http://localhost:3001', {
autoConnect: false, // connect manually when ready
reconnection: true,
reconnectionDelay: 1000,
});
A Reusable useSocket Hook
// hooks/useSocket.js
import { useEffect, useState } from 'react';
import { socket } from '../socket';
export function useSocket() {
const [isConnected, setIsConnected] = useState(socket.connected);
useEffect(() => {
socket.connect();
function onConnect() { setIsConnected(true); }
function onDisconnect() { setIsConnected(false); }
socket.on('connect', onConnect);
socket.on('disconnect', onDisconnect);
// Cleanup - CRITICAL to prevent duplicate listeners
return () => {
socket.off('connect', onConnect);
socket.off('disconnect', onDisconnect);
socket.disconnect();
};
}, []);
return { socket, isConnected };
}
Listening for Events
// hooks/useMessages.js - subscribe to a specific event
import { useEffect, useState } from 'react';
import { socket } from '../socket';
export function useMessages() {
const [messages, setMessages] = useState([]);
useEffect(() => {
function onMessage(msg) {
setMessages(prev => [...prev, msg]);
}
socket.on('message', onMessage);
// Always clean up the listener to avoid duplicates on re-render
return () => {
socket.off('message', onMessage);
};
}, []);
const sendMessage = (text) => {
socket.emit('send_message', { text, timestamp: Date.now() });
};
return { messages, sendMessage };
}
A Complete Chat Component
import { useState } from 'react';
import { useSocket } from './hooks/useSocket';
import { useMessages } from './hooks/useMessages';
function Chat() {
const { isConnected } = useSocket();
const { messages, sendMessage } = useMessages();
const [input, setInput] = useState('');
const handleSend = (e) => {
e.preventDefault();
if (input.trim()) {
sendMessage(input);
setInput('');
}
};
return (
<div>
<div>Status: {isConnected ? 'Connected' : 'Disconnected'}</div>
<ul>
{messages.map((msg, i) => (
<li key={i}>{msg.text}</li>
))}
</ul>
<form onSubmit={handleSend}>
<input value={input} onChange={e => setInput(e.target.value)} />
<button type="submit">Send</button>
</form>
</div>
);
}
Typing Indicators and Presence
export function useTyping() {
const [typingUsers, setTypingUsers] = useState([]);
useEffect(() => {
function onTyping({ user, isTyping }) {
setTypingUsers(prev =>
isTyping
? [...new Set([...prev, user])]
: prev.filter(u => u !== user)
);
}
socket.on('user_typing', onTyping);
return () => socket.off('user_typing', onTyping);
}, []);
const setTyping = (isTyping) => {
socket.emit('typing', { isTyping });
};
return { typingUsers, setTyping };
}
Handling Reconnection
useEffect(() => {
function onReconnect() {
console.log('Reconnected - re-syncing state');
// Re-fetch missed data or re-join rooms after reconnection
socket.emit('rejoin_room', { roomId });
}
socket.io.on('reconnect', onReconnect);
return () => socket.io.off('reconnect', onReconnect);
}, [roomId]);
// Socket.io auto-reconnects; use this hook to re-sync after connection drops
The Critical Rule: Always Clean Up Listeners
The most common Socket.io + React bug is duplicate event handlers. Every time a component re-renders or remounts, if you add a listener without removing the old one, you accumulate duplicates โ a single message triggers your handler multiple times. Always return a cleanup function from useEffect that removes the listener with socket.off(). This is non-negotiable for correct behavior.
Authentication
// Pass auth token when connecting
export const socket = io('http://localhost:3001', {
auth: {
token: localStorage.getItem('accessToken'),
},
autoConnect: false,
});
// Server verifies the token in a middleware before allowing the connection
// Update the token if it changes:
socket.auth.token = newToken;
socket.disconnect().connect(); // reconnect with new auth
Frequently Asked Questions
Q: Why am I receiving duplicate messages?
A: You’re adding event listeners without cleaning them up. Each re-render adds another listener. Always return a cleanup function from useEffect that calls socket.off() to remove the listener. This is the most common Socket.io + React mistake.
Q: Should I create the socket inside or outside the component?
A: Outside โ create a single shared socket instance in a separate module (socket.js) and import it. Creating it inside a component causes a new connection on every render. One shared instance is the correct pattern.
Q: How do I handle the socket in multiple components?
A: Import the shared socket instance in each component that needs it, or use a Context provider to share connection state. Each component subscribes to the events it needs (with cleanup) via its own useEffect. The single socket handles all of them.
Q: Socket.io or native WebSocket?
A: Socket.io for production React apps โ it adds automatic reconnection, fallback to long-polling, rooms, and an event-based API that saves significant work. Native WebSocket is lower-level and requires you to build reconnection and event handling yourself.
Q: How do I test real-time features?
A: Open the app in multiple browser tabs or windows to simulate multiple users and verify messages/events propagate. For automated testing, mock the socket or use a test Socket.io server. Manual multi-tab testing is the quickest way to verify real-time behavior works.
Conclusion
Adding real-time features to React with Socket.io is straightforward with the right patterns: create a single shared socket instance outside components, wrap connection and event handling in custom hooks, and ALWAYS clean up listeners in useEffect’s return function. The custom hook pattern (useSocket, useMessages, useTyping) keeps your real-time logic organized and reusable. Handle reconnection by re-syncing state after the connection recovers, and pass auth tokens on connection for secured sockets. The single most important rule is cleaning up listeners with socket.off() โ forgetting it causes duplicate handlers and is the top Socket.io + React bug. With these patterns, you can build chat, notifications, live dashboards, and collaborative features cleanly in React.
๐ You might also like
๐ Share this article




โ๏ธ Leave a Comment