I’m currently working on a fun personal project that needs lots of websocket logic, but isn’t large enough to warrant adding Redux + Sagas (or Thunks). I decided on this implementation using React’s Context API and Socket.io.
(note: To use hooks, you’ll need to update package.json
to use react@16.8.0
or later.)
We first need to create two Socket Context files, which I decided to place in the components folder:
/components/socket_context/
--- context.js - creates and exports the Context
--- index.js - creates the Provider that will wrap the app.
components/socket_context/context.js
import React, { createContext } from "react"; const SocketContext = createContext({
queueLength: 0,
positionInLine: 0,
}); export default SocketContext;
components/socket_context/index.js
import React, { useState, useEffect } from "react";
import SocketContext from "Components/socket_context/context";
import { initSockets } from "../../path/to/sockets";
// ^ initSockets is shown later onconst SocketProvider = (props) => {
const [value, setValue] = useState({
queueLength: 0,
positionInLine: 0,
});useEffect(() => initSockets({ setValue }), [initSockets]);
// Note, we are passing setValue ^ to initSockets
return(
<SocketContext.Provider value={ value }>
{ props.children }
</SocketContext.Provider>
)
};export default SocketProvider;
SocketProvider is used like so, wrapping the application:
import React from "react";
import { render } from "react-dom";
import SocketProvider from "Components/socket_context";
import App from "Components/app";const renderApp = App => {
render(
<SocketProvider>
<App/>
</SocketProvider>
document.getElementById("root")
);
}renderApp(App);
I broke apart socket event emitters and listeners in their own folders so it’s easy to keep track of everything, so the folder structure looks like this:
/sockets
---index.js
---events.js
---emit.js
With index.js
exporting initSockets
:
import io from "socket.io-client;
import { socketEvents } from "./events";
import { getQueueLength } from "./emit";
export const socket = io();export const initSockets = ({ setValue }) => {
socketEvents({ setValue });
// setValue ^ is passed on to be used by socketEvents
getQueueLength();
};
events.js
listens for events emitted from the server:
import { socket } from './index';export const socketEvents = ({ setValue }) => {
socket.on('queueLength', ({ queueLength }) => {
setValue(state => { return { ...state, queueLength } });
}); socket.on('positionInLine', ({ positionInLine }) => {
setValue(state => { return { ...state, positionInLine } });
});
};
We are using setValue
here to update the state
owned by our SocketProvider
component in response to websocket events emitted from the server. This state update is made available to all of our components via getContext
, which we will see an example of in a moment.
emit.js
contains methods that can be imported and used in any component (in an onClick method or wherever):
import { socket } from "./index";export const addClientToQueue = () => {
socket.emit('addClientIdToQueue');
};export const getQueueLength = () => {
socket.emit('queueLengthToSocket');
};export const removeUserFromQueue = () => {
socket.emit('removeUserFromQueue');
};
So if you have a onClick => addClientToQueue()
<button>, the server will listen for a socket.on(‘addClientIdToQueue’)
event being emitted from the client, and will respond accordingly.
And you can grab any updates emitted from the server inside a component by importing the SocketContext
and using React’s useContext
:
import React, { useContext } from "react";
import SocketContext from '../components/socket_context/context'const Lobby = props => {
const { positionInLine, queueLength } = useContext(SocketContext);
return (
<div>
{ queueLength }
^ This will update every time the server emits queueLength
</div>
)
}
That’s about it.
The same folder structure is used on the server side (index / emit / events) so anything emit’ed from emit.js
on the client
would be listened to in events.js
on the server
(and vice versa).
One thing that may be worth noting, in the SocketProvider,
make sure to keep the data that is to be passed via the Provider
in the state:const [value, setValue] = useState({ some: 0, stuff: 0 });
Versus passing down an object:
// DO NOT do this!
<SocketContext.Provider value={{ some: 0, stuff: 0 }}>
Passing an object that is altered will cause every component that is a consumer of the hook to re-render when any event is emitted from the server (because of this caveat with the context API).
Feel free to comment with any points of confusion and I will update this post to be clearer.
~ Fin ~