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.
js1const socket = io('https://server-domain.com');23// assuming details is the channel we want to listen to4socket.on('details', (...args) => {5 // a callback function6});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.
jsx1// SocketProvider.jsx23import React from 'react';4import socketIOClient from 'socket.io-client';56export 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.
jsx1// SocketProvider.jsx23import socketIOClient from 'socket.io-client';45const SocketProvider: React.FC = ({ children }) => {6 // we use a ref to store the socket as it won't be updated frequently7 const socket = useRef(socketIOClient('https://server-domain.com'));89 // When the Provider mounts, initialize it 👆10 // and register a few listeners 👇1112 useEffect(() => {13 socket.current.on('connect', () => {14 console.log('SocketIO: Connected and authenticated');15 });1617 socket.current.on('error', (msg: string) => {18 console.error('SocketIO: Error', msg);19 });2021 // Remove all the listeners and22 // close the socket when it unmounts23 return () => {24 if (socket && socket.current) {25 socket.current.removeAllListeners();26 socket.current.close();27 }28 };29 }, []);3031 return (32 <SocketContext.Provider value={{ socket: socket.current }}>{children}</SocketContext.Provider>33 );34};3536export 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.
jsx12// SocketProvider.jsx34export const useSocketSubscribe = (eventName, eventHandler) => {5 // Get the socket instance6 const { socket } = useContext(SocketContext);78 // 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);1314 // Remove when it unmounts15 return () \=> {16 console.log('SocketIO: removing listener', eventName);17 socket?.off(eventName, eventHandler);18 };1920 // Sometimes the handler function gets redefined21 // when the component using this hook updates (or rerenders)22 // So adding a dependency makes sure the handler is23 // up to date!24 }, [eventHandler]);2526};27
useSocketSubscribe()
is our hook. And now, components can just import this hook and use it to add listeners to the global socket.
jsx1// ExampleComponent.jsx23import React from 'react';45import { useSocketSubscribe } from './SocketProvder';67export default function ExampleComponent() {8 const [someState, setSomeState] = useState('');910 const handleSocketUpdate = (message) => {11 setSomeState(message);12 };1314 useSocketSubscribe('update', handleSocketUpdate);1516 return <div>{someState}</div>;17}18
There is one last step. We wrap the complete App in the provider.
jsx1// App.jsx23import React from 'react';4import ReactDOM from 'react-dom';56import SocketProvider from './components/SocketProvider';7import ExampleComponent from './components/ExampleComponent';89const App = () => (10 <SocketProvider>11 {/* the actual app */}12 <ExampleComponent />13 </SocketProvider>14);1516ReactDOM.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.
By Aravind Balla, a Javascript Developer building things to solve problems faced by him & his friends. You should hit him up on Twitter!