How to implement a simple CMS with push updates through websockets
We recently posted a basic tutorial on websockets and how we can use it to get a live feed of our current online users, you can read it here if you are interested. In todays tutorial, we will add a little bit more complexity but still keep it pretty basic.
We will implement a simple CMS application that connects to a websocket server and publishes updates. Our main application will subscribe to the websocket events and react to the messages comming in. More exactly, we will show a Banner with a message comming from the CMS, and also an action to hide it - without the need of refreshing the page.
Example of how it will look:
So in this tutorial, we will have three applications
- Our CMS application
- Our websocket server
- Our main application
If you don't have any project up and running. Check this guide on how you can quickly setup a React application.
Create our CMS
Websocket client
We will start by creating our Websocket client class.
class WebsocketClient { _wsClient = null; constructor() { this._wsClient = new WebSocket("ws://localhost:8085"); } sendMessage(data) { this._wsClient.send(JSON.stringify(data)); } } // Creating a Singleton export default new WebsocketClient();
This class will be our interface towards the websocket server. In the constructor, we will set up our connection. And our sendMessage() function will send message to the server.
NOTE: We are creating it as a singleton to prevent multiple open connections.
Our UI
Now we will create our UI. I will keep it very simple to avoid unnecessary complexity for this guide. Here is our component that will be our CMS:
import { useState } from "react"; import wsClient from "./WebsocketClient"; import "./App.css"; function App() { const [bannerText, setBannerText] = useState(""); const onSendBannerMsg = () => { const payload = { action: "SHOW_BANNER", message: bannerText, }; wsClient.sendMessage(payload); setBannerText(""); }; const onHideBanner = () => { const payload = { action: "HIDE_BANNER", }; wsClient.sendMessage(payload); setBannerText(""); }; return ( <div className="App"> <span className="headline">Create a banner</span> <div className="container"> <div className="container"> <input className="input" placeholder="Banner text" value={bannerText} onChange={(e) => setBannerText(e.target.value)} /> <button disabled={!bannerText} onClick={onSendBannerMsg}> Update </button> <button className="hideBtn" onClick={onHideBanner}> Hide banner </button> </div> </div> </div> ); } export default App;
Some notes
- onSendBannerMsg() will send our input string and the SHOW_BANNER action to the websocket server via the websocket client we just created
- onHideBanner() will send the HIDE_BANNER action to the websocket server
These are the messages that will be sent to our main application and decide wheather to show/hide our banner.
Create our websocket server
Let's create our project first
npm init npm install websocket uuid touch index.js
Websocket server implementation
const WebSocketServer = require("websocket").server; const http = require("http"); const PORT = 8085; const { v4: uuidv4 } = require("uuid"); const server = http.createServer(); server.listen(PORT, () => { console.log(`Server is listening on port ${PORT}`); }); wsServer = new WebSocketServer({ httpServer: server, autoAcceptConnections: true, }); wsServer.getId = () => uuidv4(); wsServer.on("connect", (ws) => { // Setting unique ID to every new client ws.id = wsServer.getId(); console.log( `A new connection has been setup, total clients: ${wsServer.connections.length}` ); ws.on("message", (raw) => { wsServer.connections.forEach((client) => { // We do not want to send to the sending client if (client.id !== ws.id) { client.send(raw.utf8Data); } }); }); ws.on("close", () => { console.log("Client disconnected"); }); });
Some notes
- We first create our http server, this is required to mount our websocket server
- We create our websocket server and setting autoAcceptConnections: true. Note: This should be false in production since it allows anyone to connect to the server. More on cors here.
- wsServer.getId = () => uuidv4(); will make sure each connection gets its own unique id when called.
- ws.on("message") will trigger when we send a message. In our case, it is our CMS that are sending messages. We will then pass this message to all the subscribers.
Main application
Let's create our main application. For this guide, we will assume you have a react application set up. As stated previously, we have a guide here on how you can very fast get a project up and running.
Banner component
Let's start with our Banner component
import styles from "./Banner.module.scss"; interface IBannerProps { isVisible: boolean; bannerText: string; } const Banner = (props: IBannerProps) => { const { isVisible, bannerText } = props; return ( <div className={`${styles.banner} ${isVisible ? styles.show : ""}`}> <span className={styles.text}>{bannerText}</span> </div> ); }; export default Banner;
Cool, there we go. As you can see, it is a fairly simple component that will get provided with a visibility value and a banner text.
Banner styling
.banner { position: absolute; left: 0; right: 0; display: flex; justify-content: center; align-items: center; background-color: rgba(0, 0, 0, 0.8); height: 70px; width: 100%; transform: translateY(-300%); transition-duration: 1000ms; z-index: 9; .text { color: white; font-size: 16px; font-weight: 650; } } .show { transform: translateY(0%); }
And now we have some styling as well. We are using an animation to make it slide down from the top to its final position.
Websocket client
We need a Websocket client in our main application as well. So let's create one similar as the one in our CMS
class WebsocketClient { _listeners = []; _wsClient = null; constructor() { this._wsClient = new WebSocket("ws://localhost:8085"); this._wsClient.onmessage = this.onMessage.bind(this); } subscribe(id, actions, callback) { const existingListener = this._listeners.find( (listener) => listener.id === id ); if (existingListener) { console.log("Already subscribing"); return; } else if (!id || !callback) { console.log("Id and Callback must be provided"); return; } else if (!actions) { console.log("Actions must be set"); return; } this._listeners.push({ id, callback, actions }); } onMessage(event) { const data = JSON.parse(event.data); if (!data.action) { console.log("No action in payload"); return; } const subscribers = this._listeners.filter((listener) => listener.actions.includes(data.action) ); subscribers.forEach((sub) => sub.callback(data)); } } export default new WebsocketClient();
This is a little bit bigger than the CMS client. We need to set up our components to subscribe to different actions, and we also need our websocket client to send the correct messages to the correct subscribers.
- Our subscribe() function will handle the subscriptions from the different components. It will make sure that it will not add the same subscriber multiple times. Each component need to pass in an id, actions to subscribe to and a callback function.
- Our onMessage() function will parse the data from the websocket server and then depending on the action, filter out the subscribers that should get the message and then call each subscribers callback function with the message from the websocket server.
Consuming it
Now it is time to consume the Banner component, and tie it all together.
In our consuming component, in our example, the App.jsx, we will set up a subscribtion to SHOW_BANNER and HIDE_BANNER actions and then send the values to our <Banner /> component.
import { useEffect, useState } from "react"; import Banner from "./components/banner/Banner"; import wsClient from "./WebsocketClient"; const App = () => { const [showBanner, setShowBanner] = useState(false); const [bannerText, setBannerText] = useState(""); useEffect(() => { wsClient.subscribe( "headerComponent", ["SHOW_BANNER", "HIDE_BANNER"], (data) => { setShowBanner(data.action === "SHOW_BANNER"); setBannerText(data.message); } ); }, []); return ( <div> <Banner isVisible={showBanner} bannerText={bannerText} /> </div> ); }; export default App;
All right, now our <App /> component will set up a subscription to all SHOW_BANNER and HIDE_BANNER actions and update the showBanner and bannerText respectively, and then send it to our <Banner /> component.
Outro
In this tutorial, we created three seperate application. One simple CMS react application, one websocket server and one main application in React as well. The CMS is sending messages through the websockets to our websocket server, and then our websocket server is passing the message to our listeners - in this case, our main application. Our main application will then show or hide a banner with the message comming from our CMS application.
So, compared to our first websocket guide, we now showed how to listen for messages as well and how we can use websockets to update our UI without the need of any refreshing etc. As stated, this was also a pretty straight forward example of how websockets can be used, we will continue providing more advanced examples and ready-to-use solutions for websockets in the near future. Stay tuned!
Thanks for reading and have a great day!