Skip to Content
Course content

223: Building a Simple Chat UI with WebSockets

Click on the "Edit" button in the top corner of the screen to edit your slide content.

When you first decide to build a chat app, your instinct is probably to figure out how to get a message from User A to User B as fast as possible. If you're coming from a traditional REST background, you might be tempted to use "short polling"—basically setting up a setInterval that hits an API endpoint every two seconds to ask, "Any new messages for me?"

The Cost of Asking "Are We There Yet?"

I've seen this approach in plenty of early-stage prototypes, and while it works for five minutes, it's a nightmare in production. Think about the overhead: every single poll is a full HTTP request. You're sending headers, negotiating TLS, and hitting the database just to get a response that says "nothing new" 99% of the time. It's incredibly wasteful for the server and creates a laggy experience for the user. If you poll every two seconds, the average message is delayed by one second. In a fast-paced conversation, that feels like an eternity.

Opening a Persistent Pipe

This is where WebSockets come in. Instead of a series of discrete requests, a WebSocket starts as an HTTP request that "upgrades" to a persistent TCP connection. I like to think of it as switching from sending letters back and forth to picking up a phone call. Once the connection is open, the server can push data to the client the millisecond it arrives, without the client asking for it.

const socket = new WebSocket('ws://chat.example.com');

socket.onmessage = (event) => {
  const data = JSON.parse(event.data);
  displayMessage(data.user, data.text);
};

function sendMessage(text) {
  socket.send(JSON.stringify({ text }));
}

The beauty here is the lack of overhead. The server simply broadcasts the message to all connected clients, and the UI updates instantly. But here is where most developers trip up: they focus so much on the connection that they forget about how they're actually updating the DOM.

The Danger of Raw innerHTML in a Chat Stream

The naive way to update your chat window is to just append the new message using innerHTML +=. It's a single line of code, and it looks great in a demo. But if you do this, you're opening a massive security hole. If a user sends a message like <img src=x onerror="alert('Hacked!')">, and you push that directly into your HTML, you've just given every user in your chat the ability to execute arbitrary JavaScript in other people's browsers. This is a textbook XSS (Cross-Site Scripting) vulnerability.

Furthermore, innerHTML += is a performance killer. Every time you use it, the browser has to destroy the entire existing DOM tree inside that element and rebuild it from scratch. In a chat room with 200 messages, adding the 201st message causes the browser to re-parse 200 lines of HTML. You'll notice the UI start to stutter as the conversation grows.

The better way is to create elements programmatically or use textContent to ensure the browser treats the message as literal text, not HTML. I prefer using insertAdjacentHTML with a sanitized template or simply building the nodes via document.createElement.

function displayMessage(user, text) {
  const msgDiv = document.createElement('div');
  msgDiv.className = 'message';
  
  const userSpan = document.createElement('span');
  userSpan.className = 'username';
  userSpan.textContent = user; // Safe from XSS
  
  const textSpan = document.createElement('span');
  textSpan.textContent = text; // Safe from XSS
  
  msgDiv.appendChild(userSpan);
  msgDiv.appendChild(textSpan);
  document.getElementById('chat-box').appendChild(msgDiv);
}

Handling the Silent Disconnect

One last thing I want to warn you about: WebSockets are fragile. They can be dropped by a flaky Wi-Fi connection, a load balancer timeout, or a server restart. The worst part is that sometimes the connection "dies" silently, and your UI still looks connected, but messages simply stop arriving. Your users will think the app is frozen.

You can't just assume the WebSocket object will always be active. I always implement a simple heartbeat (a "ping") and a reconnection strategy. If the onclose event fires, don't just log it to the console—trigger a function that attempts to reconnect after a few seconds using an exponential backoff. It's the difference between an app that feels like a toy and one that feels like professional software.




📋 Practical Task

Build a Secure, Auto-Reconnecting Chat Interface

Your goal is to create a functional chat UI that avoids the pitfalls discussed in this lesson. You will be provided with a mock WebSocket server URL (or you can use a public echo server like wss://echo.websocket.org for testing).

Requirements:

  • Secure Rendering: Create a function to append messages to a <div id="chat-window">. You must NOT use innerHTML to insert the message text; use textContent or createElement to prevent XSS.
  • Connection Lifecycle: Implement the WebSocket connection. When the connection is successfully opened, update a status indicator on the page to "Connected".
  • Resilience: Implement a reconnection mechanism. If the onclose event is triggered, the app should attempt to reconnect every 5 seconds until the connection is restored.
  • Interaction: Create an input field and a send button that transmits a JSON string containing the message and a username.
Rating
0 0

There are no comments for now.

to be the first to leave a comment.