Skip to content

Using Socket.IO With React Hooks

React/

It can get quite messy when you are working with WebSockets in React. You have to add a handler function for each event and remember to remove them as well.

You can write a custom hook to handle this process for you. This way you will never forget to remove an event listener you no longer need.

Creating a Custom Hook for Socket Events

Before I show you how to handle the events, you need to set up the connection that you can access in the rest of your application.

main.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import io from 'socket.io-client';

import App from './App';

const socketServerAddress = 'http://localhost:3000';
export const socket = io(socketServerAddress);

ReactDOM.createRoot(document.getElementById('root') as HTMLElement).render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
);
  • The socketServerAddress should contain the URL of your WebSockets server
  • By using export, the socket can imported in other parts of your code

Alternatively, you can set up a context, and access the socket that way.

Now you can create a custom hook to add and remove event listeners on the socket.

This hook can accept an array of events that contain objects. These objects have a name property and a function that is called when the event is triggered.

hooks/useSocketEvents.ts
import { useEffect } from 'react';
import { socket } from '../main';

export interface Event {
  name: string;
  handler(...args: any[]): any;
}

export function useSocketEvents(events: Event[]) {
  useEffect(() => {
    for (const event of events) {
      socket.on(event.name, event.handler);
    }

    return function () {
      for (const event of events) {
        socket.off(event.name);
      }
    };
  }, []);
}

This hook loops over all events inside the events argument to add or remove event listeners.

  • Each event must have a name and a function that handles it
  • You can use ...args: any[] to allow the handler function have many parameters of different types
  • With the help of useEffect, you can register all events by calling socket.on
  • Passing an empty array ([]) to useEffect, makes sure that this function is called just once, when component mounts
  • It is important to remove the event listeners with socket.off when your component demounts
  • The function that useEffect returns cleans up the event listeners to avoid duplicates

Using the Hook

Let’s set up a simple application that can receive a list of messages after socket connects successfully and listens to new incoming messages.

App.tsx
import { useState, useEffect } from 'react';
import { Event, useSocketEvents } from './hooks/useSocketEvents';

interface Message {
  id: string;
  content: string;
}

export default function App() {
  const [messages, setMessages] = useState<Message[]>([]);
  const events: Event[] = [
    {
      name: 'chat:message',
      handler(message: Message) {
        setMessages(prevMessages => [...prevMessages, message]);
      },
    },
    {
      name: 'chat:message_list',
      handler(messages: Message[]) {
        setMessages(messages);
      },
    },
  ];

  useSocketEvents(events);

  return (
    <>
      <p>Latest messages:</p>

      {messages.length > 0 && (
        <ul>
          {messages.map(message => {
            return <li key={message.id}>{message.content}</li>;
          })}
        </ul>
      )}
    </>
  );
}

Here you can see how to register two events — chat:message and chat:message_list. Each event has a name and handler function.

The name and handler contents are up to you. You can choose any name you like and make the handler function do whatever needs to be done when this event is triggered.

In case of chat:message event, which is fired when a new message arrives, the handler appends the new message to previously received messages. It is important to create a new array or else the state becomes stale and the component doesn’t re-render.