Using Socket.IO With React Hooks
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.
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.
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 callingsocket.on
- Passing an empty array (
[]
) touseEffect
, 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.
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.