100% Free Forever
AI-Powered Learning
Industry Expert Content
Certificates & Badges
Learn At Your Own Pace
Programming

Socket.IO with React

Integrate Socket.IO cleanly into React using a context provider, custom hooks, and correct connection lifecycle management.

Practical Socket.IOIntermediate9 min readJul 10, 2026
Analogies

Using Socket.IO with React

The single most common React mistake with Socket.IO is creating a new connection inside a component's render body or inside useEffect with a dependency array that changes often, which opens a fresh socket on every re-render and leaks connections; the correct pattern is to create the socket once, outside any component (a module-level singleton) or inside a useEffect with an empty dependency array [] that returns a cleanup function calling socket.disconnect(), ensuring exactly one connection per component mount, not per render.

🏏

Cricket analogy: It's like a broadcaster accidentally opening a brand-new satellite uplink every time the camera angle changes instead of once per session — you'd end up paying for and juggling dozens of redundant feeds instead of one stable connection reused throughout the match.

A Custom Hook and Context Provider Pattern

Once the singleton connection concern is solved, the cleanest React architecture wraps the socket in a SocketProvider using createContext, exposing the socket instance (and derived state like isConnected) to the component tree, so individual components use a useSocket() hook to access it rather than importing the raw socket module everywhere, which keeps event subscriptions colocated with the components that care about them; each component that listens for an event should register its socket.on inside its own useEffect and clean up with socket.off in the effect's return function, since forgetting the off call is the second most common source of duplicate-handler bugs, especially with React StrictMode's intentional double-invoke in development.

🏏

Cricket analogy: It's like a team's shared dressing-room radio broadcast — every player (component) tunes in via their own earpiece (useEffect) rather than each player needing to physically plug into the broadcast van, and each player removes their earpiece (off) when they leave the huddle so they don't keep hearing calls meant for the field.

Handling Reconnection State in the UI

Socket.IO's client automatically attempts reconnection by default, but the UI should reflect this rather than silently failing, which means listening for connect, disconnect, and connect_error events to drive a small state machine (connecting | connected | reconnecting | disconnected) that a banner or toast component reads from context; it's also important to re-fetch or re-sync any state that might have changed while disconnected (like unread notification counts) inside the connect handler specifically when socket.recovered is false, since Socket.IO's connection state recovery feature can sometimes replay missed events automatically but should not be assumed to always succeed.

🏏

Cricket analogy: It's like a rain-delay indicator on a cricket broadcast — viewers see an explicit 'players off the field, play will resume shortly' banner rather than the feed just silently freezing, and once play resumes the broadcast explicitly re-syncs the scoreboard rather than assuming nothing changed.

jsx
// SocketProvider.jsx
import { createContext, useContext, useEffect, useRef, useState } from 'react';
import { io } from 'socket.io-client';

const SocketContext = createContext(null);

export function SocketProvider({ children, token }) {
  const socketRef = useRef(null);
  const [isConnected, setIsConnected] = useState(false);

  if (!socketRef.current) {
    socketRef.current = io(process.env.REACT_APP_SOCKET_URL, {
      auth: { token },
      autoConnect: false,
    });
  }

  useEffect(() => {
    const socket = socketRef.current;
    socket.connect();

    const onConnect = () => setIsConnected(true);
    const onDisconnect = () => setIsConnected(false);
    socket.on('connect', onConnect);
    socket.on('disconnect', onDisconnect);

    return () => {
      socket.off('connect', onConnect);
      socket.off('disconnect', onDisconnect);
      socket.disconnect();
    };
  }, []);

  return (
    <SocketContext.Provider value={{ socket: socketRef.current, isConnected }}>
      {children}
    </SocketContext.Provider>
  );
}

export const useSocket = () => useContext(SocketContext);

Under React 18 StrictMode in development, effects mount, unmount, then mount again, which will call socket.disconnect() then immediately reconnect. This is expected and mirrors production behavior after any real remount — don't 'fix' it by removing the cleanup function, or you'll leak real connections in production.

  • Create the Socket.IO connection once per component tree, not per render — use a ref, module singleton, or an empty-dependency useEffect.
  • Always clean up with socket.disconnect() (or socket.off for individual listeners) in the effect's return function.
  • A SocketProvider + useSocket() context pattern colocates event subscriptions with the components that need them.
  • Register listeners in useEffect and remove them with socket.off to avoid duplicate handlers, especially under StrictMode's double-invoke.
  • Drive UI state (connecting/connected/reconnecting/disconnected) from connect, disconnect, and connect_error events rather than assuming silent success.
  • Re-sync state on reconnect, especially when socket.recovered is false, since automatic connection state recovery isn't guaranteed.
  • React StrictMode's mount-unmount-mount cycle in development is expected behavior, not a bug in your cleanup logic.

Practice what you learned

Was this page helpful?

Topics covered

#Programming#SocketIOStudyNotes#SocketIOWithReact#Socket#React#Custom#Hook#Networking#StudyNotes#SkillVeris