Create A 'Control Room' for Client-Side Rendered Apps
Learn how to build a control room system for client-side rendered applications using WebSockets to manage app state, maintenance windows, and version updates in real-time.
Original Publication: This article was originally published on JavaScript in Plain English (Medium) on February 15, 2021. It has been adapted for this portfolio while maintaining the original content and adding modern formatting.
I'm not sure about the title of this post, I thought about it for 24 hours and didn't find anything better, so just called it "Creating A Control Room for Client-Side Rendered Frontend Apps!" ¯\_(ツ)_/¯.
This pattern is beneficial if you need to communicate some States
with your frontend App and as we know for the client-side rendered Apps there are some challenges. In this post, I'll go over one example to give you an overall picture.
Client-side Rendered Apps
The easiest way to deploy a frontend App is to build it and push the assets (Javascript, CSS, HTML, etc) to your server(or serverless) and let the App renders on the client-side(vs server-side rendered). A very common example is building a React App and push it to AWS S3 and then that's it! But there is a problem with this approach which you can't control the state of your App after it's rendered on the client-side which is a very important issue if you're working on an enterprise App that has a lot of users every day.
For example, if you need to put your App in a maintenance window because of some API migration or some infrastructure tasks on your API service, how do you handle that scenario? Do you release a new version to update your App state?
Another example is whenever you release a new version, uses who are using your App don't have any idea about the new version until they refresh their page, as a result, if you've deployed a new version of your API service that needs some changes on Frontend, how do you let users know they need to refresh their page to get the latest version?
A generic solution that works for all scenarios
The bottom line is we want to create a control room for our frontend App. The solution I want to talk about it here is one of many solutions out there and it doesn't necessarily mean the best one, but something you can consider if you're working on an App that needs that level of controls.
My approach here is let's add a Web Socket Server
that can sends messages
to our frontend App and then the frontend App can handle those messages.
Before we go into the details let's take a look at my demo App.
A New Version Available
For the purpose of the demo, I created a super simple React App(almost empty) which you can see the deployed version here and take a look at the code you can check this repository. Let's take a look at the deployment scripts:
"prepush":"yarn build",
"push": "aws s3 sync --delete build s3://websocket-app --profile websocket",
"postpush": "node postPush.js"
The deployment script includes building the assets and then push it to S3. We're not gonna go over React's App deployment in this post but if you're interested you can take a look at this post. Our focus is on what happens in postpush
, our goal is to send a message to our Frontend App to let users know that there is a new version available and they need to refresh their page to get that.
How to?
Before I go over the code I should mention that in this post I don't want to go over a production-ready solution because it can be different based on different delivery solutions and pipelines and I just want to give you an overall picture of this approach.
To implement this approach I used Web Sockets
. If you never worked with Web Sockets
all you need to know is in comparison to HTTP protocol there aren't any requests. Clients
open a connection to the server
and then they wait for messages from the server(exactly what we want!) If you like to learn more about web sockets you can read this article.
For the frontend side, I created a Context
that wraps the whole App at the top level and will be responsible for opening the connection to our Web Socket Server
and whenever it received a message take action based on the messageId
and more data provided by the message. When our App loads, inside the useEffect
we connect to our Web Socket Server and then we listen to the all incoming messages:
useEffect(() => {
socket.current.onopen = function (e) {
console.info("[open] Connection established");
};
socket.current.onerror = function (error) {
console.error(`[error] ${error}`);
};
socket.current.onmessage = function (event) {
const data = JSON.parse(event.data);
switch (data.messageId) {
case "new-version":
notify.show(
<div>
<h2>Sorry to intrupt you 🙈</h2>
<p>
A new version of our App is available and to get the latest
features you should refresh your page
</p>
<button onClick={() => window.location.reload()}>Reload</button>
</div>,
"warning",
-1
);
break;
case "maintenance":
if (data.status === "on") {
notify.show(
<div>
<h2>Sorry to intrupt you 🙈</h2>
<p>{data.message}</p>
<button onClick={() => notify.hide()}>Ok</button>
</div>,
"warning",
-1
);
setMaintWindow(true);
} else {
setMaintWindow(false);
}
break;
default:
break;
}
};
}, []);
You can see the complete code for the frontend App here.
Now on the backend side for purpose of this demo I created a simple HTTP and Web Socket Server. The HTTP server is to be able to send some events to the control room service (I like it, it's a fancy name, control room!😅) and then the control room service Broadcasts the message to all connected clients. Let's take a look at the server code which is really minimal:
const WebSocket = require("ws");
const express = require("express");
const app = express();
const http = require("http");
const server = http.createServer(app);
const PORT = process.env.PORT || 8080;
const path = require("path");
app.use(express.json());
let wss;
app.post("/api/message", function (req, res) {
const { messageId, messageBody } = req.body;
switch (messageId) {
case "new-version":
broadCastToAllClients(
wss,
JSON.stringify({
messageId,
newVersion: messageBody.version,
})
);
return res.send("ok");
case "maintenance":
broadCastToAllClients(
wss,
JSON.stringify({
messageId,
message: messageBody.message,
status: messageBody.status,
})
);
return res.send("ok");
default:
return res.status(400).send("what do you mean?");
}
});
app.get("/", (req, res) => {
res.sendFile(path.resolve(__dirname, "index.html"));
});
app.get("/api/ping", (req, res) => {
res.send("PONG");
});
server.listen(PORT, (error) => {
if (error) {
console.error(`Something went wrong on Express Server 💔`);
} else {
console.log(`Express server is running on port ${PORT}`);
wss = new WebSocket.Server({ server });
}
});
function broadCastToAllClients(wsServer, message) {
wsServer.clients.forEach(function each(client) {
if (client.readyState === WebSocket.OPEN) {
client.send(message);
}
});
}
Live Demo: If you like to try it you can open the frontend App and the Web Socket service on two separate tabs and try it:
- Frontend App: http://websocket-app.s3-website-us-east-1.amazonaws.com/
- The Web Socket Service: https://thawing-depths-04303.herokuapp.com/
You can try those simple commands and see the result on the Frontend App.
Now that you tried the demo Apps let's go back to our publish
scripts and see what does postpush
does:
"postpush": "node postPush.js"
And the postPush.js
:
const fetch = require("node-fetch");
const body = {
messageId: "new-version",
messageBody: {
version: "1.1.1",
},
};
fetch("https://thawing-depths-04303.herokuapp.com/api/message", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
})
.then((resp) => resp.text())
.then((data) => console.log(data))
.catch((error) => console.log("Ooops", error));
As you can see in this script all we do is send a POST
request to our Control Room Service
to let it know that there was a new frontend release, so it can broadcast the message to all connected clients.
Conclusion
The client-side rendered Apps come with some challenges, and in this post, I tried to show you an approach that you can use to have more control on your them. Based on your pipelines you might be able to implement it in different ways.
I hope you enjoyed my Control Room Service 😉
Stay Safe 🤿
About this article: This technical tutorial was originally published on JavaScript in Plain English (Medium) in February 2021. It demonstrates advanced patterns for real-time communication and state management in client-side rendered applications that remain relevant in modern web development.