Handling Live Stock Data and Messages with WebSockets in JavaScript

Nitish Kumar Singh

May 31, 2025

Ever wondered how trading apps show live stock prices and also send or receive custom messages in real time? Here, I’ll explain a simple JavaScript code that does both.

Photo by Christopher Gower on Unsplash

If you're planning to build a real-time stock tracking system or just want to understand how WebSocket works with both live data and messages, you're in the right place. Let’s go through the code in simple terms.

Let me explain what I actually want to build in the points below:

  • A class that handles everything. We just call different methods of this class and get results back.
  • It connects to the server when we want, or automatically gets connected when we use it for any server result.
  • Handles reconnect on any failure up to limited trials.
  • Handles subscription and unsubscription of stock tokens.
  • Sends custom messages and gets results back gracefully by queuing messages.
  • Always updates the status of connection to the user.

Let’s get started with the code.

We will create a class FeedSocket, and in its constructor, initialize some properties as below.

import { trader } from "./TraderDetails.js";
import { CONSTS } from "./UTILS.js";

export default class FeedSocket{
    constructor(){
        this.tokens = new Map(); // tokens to listeners map
        this.msgQueue = new Map(); // For queuing messages
        this.retryCount = 0; // for reconnect trails
    }
    // rest of code goes here...
}

We import trader to show trader details and socket connection status, and CONSTS for different predefined constants.

We use a map tokens to avoid subscription of the same token multiple times and another map msgQueue to queue custom messages.

We have created a method of this class connect that handles connecting to the server gracefully, avoids multiple connections, sets required callback listeners, and sends any pending messages to the server.

connect = ()=>{        
    if(this.socket && (this.socket.readyState === this.socket.CONNECTING || this.socket.readyState === this.socket.OPEN)) return;
    this.socket = new WebSocket("ws://localhost:3001"); 
    this.socket.onopen = () => {
        console.log("Socket openned");
        this.socket.onmessage = this.onMessage;
        this.socket.onclose = this.onClose;
        this.socket.onerror = this.onError;
        [...this.msgQueue.keys()].forEach(msgId => 
            this.socket.send(JSON.stringify({action:CONSTS.FOR_MESSAGES,msgId,data:this.msgQueue.get(msgId).data})));
    };
}

Below are the callback methods that we set on socket. Here, I have implemented the code for reconnect only on failure of connection after a while, not on initial connection.

onClose = (event) => {
    let reason = `Socket closed (code: ${event.code})`;
    const abnormal = !event.wasClean || event.code !== 1000;
    if (abnormal) reason = `Unexpected disconnect (code ${event.code || 1006}).`;
    console.log(reason);

    delete this.socket;
    delete this.clientId;

    if (abnormal && this.retryCount < 3) {
        const delay = 1000 * Math.pow(2, this.retryCount); // 1s, 2s, 4s
        this.retryCount++;
        trader.update?.(`Reconnecting in ${delay / 1000}s (attempt ${this.retryCount}/3)…`);
        setTimeout(() => this.connect(), delay);
    } else if (abnormal) {
        trader.update?.("Failed to reconnect after 3 attempts.");
    }
};

onError = (err) => {
    console.error("Socket error", err);
};

disconnect = () => {
    if (this.socket) this.socket.close(1000, "Closed by user");
};

Method disconnect is used for manually closing the socket by the user. We report about every trial of reconnect to trader, and it handles showing it to the user.

Below is the subscribe method we use to subscribe to multiple tokens.

subscribe = (stocks,listener)=>{
    if (this.socket && this.socket.readyState === this.socket.OPEN) {            
        const subscribers = [];
        stocks.forEach(stock => {
            let listeners = this.tokens.get(stock.token);
            if(!listeners) {
                listeners = new Set();
                this.tokens.set(stock.token,listeners);
                subscribers.push(stock);
            }
            listeners.add(listener);
        });
        if(subscribers.length>0) this.socket.send(JSON.stringify({action:CONSTS.FOR_SUBSCRIPTION,data:subscribers}));
    }
}

I used a Set to store listeners for each token because I need to receive feed updates in multiple React components like Charts, WatchList, and FeedVisualizer. Using a Set ensures that we only subscribe to a token once, even if multiple components are listening to it.

I didn’t add a check for whether the socket is open inside the subscribe method. That’s because, in my case, the socket is already opened through the sendMessage method, which is always called first from the WatchList component before any subscribe calls happen.

I’ve already written two blogs — one on Building the WatchList Component, and another on Converting Tick Data Into Candlestick Data For Charting.

We use the below unsubscribe method to stop listening to tokens.

unsubscribe = (stocks,listener)=>{
    const unsubscribers = [];
    stocks.forEach(stock => {
        let listeners = this.tokens.get(stock.token);
        if(listeners) {
            listeners.delete(listener);
            if(listeners.size === 0) {
                this.tokens.delete(stock.token,listeners);
                unsubscribers.push(stock);
            }
        }
    });
    if(unsubscribers.length>0) this.socket.send(JSON.stringify({clientId:this.clientId,action:CONSTS.FOR_UN_SUBSCRIPTION,data:unsubscribers}));
}

This method gracefully handles removing a listener for a particular component, and if no more components are listening to this token, then it also unsubscribes from the server.

The other two methods, sendMessage and onMessage, handle sending messages and delivering the response to the correct callback, and also pass feed ticks to their respective listeners.

sendMessage = (data,callback)=> {    
    const msgId = `msgId${this.msgQueue.size + 1}-${Date.now().toString(16)}`;
    this.msgQueue.set(msgId,{data,callback});
    if(this.socket && this.socket.readyState === this.socket.OPEN) this.socket.send(JSON.stringify({action:CONSTS.FOR_MESSAGES,msgId,data}));
    else this.connect();
}
onMessage = async (event) => {
    const msg = JSON.parse(event.data);     
    if(msg.action === CONSTS.FOR_FEEDS){ // Feeds 
        let listeners = this.tokens.get(msg.data.token);
        if(listeners) listeners.forEach(listener => listener(msg.data));
    }else if (msg.action === CONSTS.FOR_MESSAGES) { // Messages 
        const message = this.msgQueue.get(msg.msgId);
        message?.callback(msg);
        this.msgQueue.delete(msg.msgId);
    }else if(msg.action === CONSTS.FOR_CLIENT_ID) {
        this.clientId = msg.data;                        
        Object.assign(trader,msg.user);   
        trader.update?.(); 
    }
}

In sendMessage, we first add the message to the queue for delivery of its response to its correct callback and send this message to the server if the socket is open. Otherwise, we call the connect method, which handles connecting to the server and sending this pending message.

In onMessage, we send feeds to their respective listeners and the message's response to its correct callback by getting the callback using its msgId.

I am creating msgId using the current time and the size of the queue, but for more uniqueness of msgId, you can use the uuid package.

So this is how I’m using the web socket in my app — by creating the FeedSocket class that handles sending and receiving messages, listening to feeds of stocks, and updating different components like WatchList and Chart.

I’ve already written two blogs — one for WatchList Creation and another for Candle-Stick Data Building — before this, so this one is related to those two.

I hope you enjoyed building this class with me, understood everything clearly, learned some useful logic, and found this post informative.

Thanks for reading! 🤝 Happy Coding!

Published on May 31, 2025
Comments (undefined)

Read More