Custom React hook for websocket updates

Banner image for Custom React hook for websocket updates

Creating reusable components is the main plus point when we are working with React. And hooks let us sprinkle powers to those components. Adding some state to the component, for example.

In this article, we will look at how we can create a custom hook, which powers the component to subscribe and unsubscribe to a websocket so that it can listen to all the events in a channel.

We will be using socket.io library. And we don't cover the steps involved to build a server which accepts socket connections.

The actual process of building a socket client is very simple. We have to create a socket and initialize it, and then add listeners to the channels which respond to events.

js
1const socket = io('https://server-domain.com');
2
3// assuming details is the channel we want to listen to
4socket.on('details', (...args) => {
5 // a callback function
6});
7

When it comes to doing this in React, we might have some decisions to make:

  • Do we store the socket instance in state?
  • Or a ref because we wont have to update it.
  • What if I have multiple components that want to use the socket? Maybe I store near the root and pass the socket via props to the components that need it.
  • A lot of passing props. Should I use context now?

That was exactly my thought process and this is solution I ended up with - we store the socket instance in the context and let the components subscribe using hooks.

The pattern we use here is inspired by Kent's How to use Context effectively post. You might want to have a look for extra clarity.

Creating the context

Lets kick things off by creating the context and exporting the Provider and the hooks so that the components can use them.

jsx
1// SocketProvider.jsx
2
3import React from 'react';
4import socketIOClient from 'socket.io-client';
5
6export const SocketContext = React.createContext({ socket: null });
7

That's our Context and we initialize the socket in the context with null.

Now let's create the Provider, the component which is responsible for initializing the socket and putting it in the context so that the other components can use.

jsx
1// SocketProvider.jsx
2
3import socketIOClient from 'socket.io-client';
4
5const SocketProvider: React.FC = ({ children }) => {
6 // we use a ref to store the socket as it won't be updated frequently
7 const socket = useRef(socketIOClient('https://server-domain.com'));
8
9 // When the Provider mounts, initialize it 👆
10 // and register a few listeners 👇
11
12 useEffect(() => {
13 socket.current.on('connect', () => {
14 console.log('SocketIO: Connected and authenticated');
15 });
16
17 socket.current.on('error', (msg: string) => {
18 console.error('SocketIO: Error', msg);
19 });
20
21 // Remove all the listeners and
22 // close the socket when it unmounts
23 return () => {
24 if (socket && socket.current) {
25 socket.current.removeAllListeners();
26 socket.current.close();
27 }
28 };
29 }, []);
30
31 return (
32 <SocketContext.Provider value={{ socket: socket.current }}>{children}</SocketContext.Provider>
33 );
34};
35
36export default SocketProvider;
37

We haven't added all the listeners yet. We need to let components listen to the websocket updates. So lets create a custom hook.

jsx
1
2// SocketProvider.jsx
3
4export const useSocketSubscribe = (eventName, eventHandler) => {
5 // Get the socket instance
6 const { socket } = useContext(SocketContext);
7
8 // when the component, *which uses this hook* mounts,
9 // add a listener.
10 useEffect(() => {
11 console.log('SocketIO: adding listener', eventName);
12 socket.on(eventName, eventHandler);
13
14 // Remove when it unmounts
15 return () \=> {
16 console.log('SocketIO: removing listener', eventName);
17 socket?.off(eventName, eventHandler);
18 };
19
20 // Sometimes the handler function gets redefined
21 // when the component using this hook updates (or rerenders)
22 // So adding a dependency makes sure the handler is
23 // up to date!
24 }, [eventHandler]);
25
26};
27

useSocketSubscribe() is our hook. And now, components can just import this hook and use it to add listeners to the global socket.

jsx
1// ExampleComponent.jsx
2
3import React from 'react';
4
5import { useSocketSubscribe } from './SocketProvder';
6
7export default function ExampleComponent() {
8 const [someState, setSomeState] = useState('');
9
10 const handleSocketUpdate = (message) => {
11 setSomeState(message);
12 };
13
14 useSocketSubscribe('update', handleSocketUpdate);
15
16 return <div>{someState}</div>;
17}
18

There is one last step. We wrap the complete App in the provider.

jsx
1// App.jsx
2
3import React from 'react';
4import ReactDOM from 'react-dom';
5
6import SocketProvider from './components/SocketProvider';
7import ExampleComponent from './components/ExampleComponent';
8
9const App = () => (
10 <SocketProvider>
11 {/* the actual app */}
12 <ExampleComponent />
13 </SocketProvider>
14);
15
16ReactDOM.render(<App />, document.getElementById('app'));
17

That's all. You now have a frontend where the components can choose to listen to the socket updates using the custom hook we built.

Hope that helps.

Have a good day.

Aravind Balla

By Aravind Balla who is a friendly human, building things for himself, and sometimes for others. You should hit him up on Twitter!

Get letters from me 🙌

Get a behind-the-scenes look on the stuff I build, articles I write and podcast episodes which make you a more effective builder.

Read the archive 📬

One email every Tuesday. No more. Maybe less.