<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0">
<channel>
<title>Coder Legion - Recent posts tagged websockets</title>
<link>https://coderlegion.com/tag/websockets</link>
<description>Powered by Question2Answer</description>
<item>
<title>Building a Real Time Notification System with React and WebSockets</title>
<link>https://coderlegion.com/31801/building-a-real-time-notification-system-with-react-and-websockets</link>
<description>&lt;p&gt;Modern web applications need to communicate with users instantly. Whether someone receives a new message, a payment confirmation, an order update, a project assignment, or a security alert, they expect the information to appear without refreshing the page. Real-time notifications have become an essential feature of dashboards, social media platforms, e-commerce applications, collaboration tools, and customer management systems.&lt;/p&gt;
&lt;p&gt;Traditional web applications often rely on polling to check whether new information is available. Although polling is straightforward to implement, it can generate unnecessary HTTP requests and introduce delays. WebSockets provide a more efficient communication model by establishing a persistent, bidirectional connection between the browser and the server.&lt;/p&gt;
&lt;p&gt;When combined with React, WebSockets make it possible to build responsive notification interfaces that update automatically as events occur.&lt;/p&gt;
&lt;p&gt;In this article, we will explore how to design, implement, secure, test, and scale a real-time notification system using React, JavaScript, Node.js, and WebSockets. We will also discuss notification persistence, unread counters, reconnection strategies, database integration, and production deployment.&lt;/p&gt;
&lt;h3 id=&quot;1-understanding-real-time-notifications&quot;&gt;1. Understanding Real-Time Notifications&lt;/h3&gt;
&lt;p&gt;A real-time notification is a message delivered to a user when a particular event happens. Unlike traditional alerts that require the user to refresh a page or revisit a section, real-time notifications can appear immediately while the application remains open.&lt;/p&gt;
&lt;p&gt;Consider a project management application. When a team member assigns a task to another employee, the backend records the assignment and sends a notification to the appropriate user. If that user is currently online, the notification appears in the React interface almost immediately.&lt;/p&gt;
&lt;p&gt;Common notification events include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;New messages in a conversation.&lt;/li&gt;
&lt;li&gt;Changes to an order or delivery status.&lt;/li&gt;
&lt;li&gt;Payment confirmations and billing alerts.&lt;/li&gt;
&lt;li&gt;New comments, mentions, or reactions.&lt;/li&gt;
&lt;li&gt;Task assignments and project updates.&lt;/li&gt;
&lt;li&gt;System maintenance and security warnings.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;A reliable notification system should do more than display a temporary message. It should identify the recipient, preserve important events, maintain unread counts, support marking notifications as read, and recover gracefully when a connection is interrupted.&lt;/p&gt;
&lt;p&gt;The goal is to deliver a consistent experience whether users are actively interacting with the application or returning after being offline.&lt;/p&gt;
&lt;h3 id=&quot;2-what-are-websockets-&quot;&gt;2. What Are WebSockets?&lt;/h3&gt;
&lt;p&gt;WebSockets are a communication protocol that enables two-way communication between a client and a server over a persistent connection. After the initial connection is established, both sides can send messages independently without creating a new HTTP request for every update.&lt;/p&gt;
&lt;p&gt;According to the MDN WebSockets documentation, this communication model allows applications to exchange data without repeatedly polling the server.&lt;/p&gt;
&lt;p&gt;Reference: &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebSocket&quot;&gt;https://developer.mozilla.org/en-US/docs/Web/API/WebSocket&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;In a traditional polling approach, the browser might request notification updates every five seconds. If thousands of users keep polling, the backend must process a large number of requests even when no new notifications exist.&lt;/p&gt;
&lt;p&gt;With WebSockets, the browser establishes a connection and waits. When an event occurs, the server sends the notification through that connection.&lt;/p&gt;
&lt;h4 id=&quot;websockets-versus-polling&quot;&gt;WebSockets versus polling&lt;/h4&gt;
&lt;p&gt;&lt;strong&gt;Polling&lt;/strong&gt; is easy to understand and works with almost any HTTP backend. However, it can waste resources when updates are infrequent and may introduce delays between polling intervals.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Long polling&lt;/strong&gt; keeps an HTTP request open until the server has an update or a timeout occurs. It can reduce unnecessary requests but introduces additional request-management complexity.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;WebSockets&lt;/strong&gt; maintain a persistent connection and support bidirectional messaging. They are particularly useful when updates must be delivered quickly or when the client also needs to send real-time events.&lt;/p&gt;
&lt;p&gt;Server-Sent Events (SSE) are another option when the browser primarily needs one-way updates from the server. The right choice depends on the application's communication requirements.&lt;/p&gt;
&lt;p&gt;For a notification system that needs low-latency delivery and may later support features such as live chat or collaborative updates, WebSockets are a strong option.&lt;/p&gt;
&lt;h3 id=&quot;3-designing-the-system-architecture&quot;&gt;3. Designing the System Architecture&lt;/h3&gt;
&lt;p&gt;Before writing code, it is important to understand the responsibilities of each component.&lt;/p&gt;
&lt;p&gt;A practical architecture contains five major parts:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;React frontend:&lt;/strong&gt; Displays notifications, unread counts, connection status, and user interactions.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;WebSocket server:&lt;/strong&gt; Maintains active connections and delivers events to authenticated users.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Application backend:&lt;/strong&gt; Generates notifications when business events occur.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Database:&lt;/strong&gt; Stores notification records so users can access them after reconnecting.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Message broker:&lt;/strong&gt; Coordinates event delivery across multiple backend instances when the application scales.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;The basic event flow looks like this:&lt;/p&gt;
&lt;p&gt;A user performs an action, such as creating an order. The backend validates the action and stores the resulting business data. It then creates a notification event for the intended recipient. The WebSocket layer sends the event to the user's active connection, and React updates the interface.&lt;/p&gt;
&lt;p&gt;If the recipient is offline, the notification remains in persistent storage and can be retrieved through an HTTP API when the user returns.&lt;/p&gt;
&lt;p&gt;This distinction is essential: WebSockets provide a delivery channel, but they should not be the only place where important notification data exists.&lt;/p&gt;
&lt;h3 id=&quot;4-choosing-the-technology-stack&quot;&gt;4. Choosing the Technology Stack&lt;/h3&gt;
&lt;p&gt;For this implementation, we will use a JavaScript-based stack.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;&lt;strong&gt;React:&lt;/strong&gt; Builds the notification interface.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Node.js:&lt;/strong&gt; Runs the server-side JavaScript environment.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Express:&lt;/strong&gt; Provides HTTP routes for the backend.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;ws:&lt;/strong&gt; Provides a WebSocket server implementation for Node.js.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;PostgreSQL or another database:&lt;/strong&gt; Stores notifications and read states.&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Redis or a message broker:&lt;/strong&gt; Supports cross-instance event distribution when required.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The &lt;code&gt;ws&lt;/code&gt; library is a useful choice for demonstrating the underlying WebSocket protocol directly. Socket.IO is another popular option that adds features such as automatic reconnection and fallback transports. However, Socket.IO and native WebSockets are different protocols and are not interchangeable without compatible client and server implementations.&lt;/p&gt;
&lt;p&gt;We will use the &lt;code&gt;ws&lt;/code&gt; library for the server and the browser's native &lt;code&gt;WebSocket&lt;/code&gt; API for the React client.&lt;/p&gt;
&lt;h3 id=&quot;5-creating-the-backend-websocket-server&quot;&gt;5. Creating the Backend WebSocket Server&lt;/h3&gt;
&lt;p&gt;First, create a project directory and install the required dependencies.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;mkdir realtime-notifications
cd realtime-notifications
npm init -y
npm install express ws cors
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a file named &lt;code&gt;server.js&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;The following example demonstrates a basic WebSocket server that associates connections with users and sends notifications to the correct recipient. It uses a simplified demonstration authentication mechanism; production applications must replace it with real authentication and authorization.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const express = require(&quot;express&quot;);
const http = require(&quot;http&quot;);
const WebSocket = require(&quot;ws&quot;);
const cors = require(&quot;cors&quot;);

const app = express();

app.use(cors({
  origin: &quot;http://localhost:5173&quot;,
}));

app.use(express.json());

const server = http.createServer(app);

const wss = new WebSocket.Server({
  server,
  path: &quot;/ws&quot;,
});

const clients = new Map();

wss.on(&quot;connection&quot;, (socket, request) =&amp;gt; {
  // Demonstration only: do not trust a user ID
  // supplied directly by a client in production.
  const url = new URL(
    request.url,
    &quot;http://localhost:3000&quot;
  );

  const userId = url.searchParams.get(&quot;userId&quot;);

  if (!userId) {
    socket.close(1008, &quot;Authentication required&quot;);
    return;
  }

  if (!clients.has(userId)) {
    clients.set(userId, new Set());
  }

  clients.get(userId).add(socket);

  socket.on(&quot;close&quot;, () =&amp;gt; {
    const connections = clients.get(userId);

    if (!connections) return;

    connections.delete(socket);

    if (connections.size === 0) {
      clients.delete(userId);
    }
  });

  socket.on(&quot;error&quot;, (error) =&amp;gt; {
    console.error(&quot;WebSocket error:&quot;, error);
  });
});

function sendNotification(userId, notification) {
  const connections = clients.get(String(userId));

  if (!connections) return;

  const message = JSON.stringify({
    type: &quot;notification&quot;,
    payload: notification,
  });

  for (const socket of connections) {
    if (socket.readyState === WebSocket.OPEN) {
      socket.send(message);
    }
  }
}

app.post(&quot;/api/notifications/demo&quot;, (req, res) =&amp;gt; {
  const { userId, title, message } = req.body;

  if (!userId || !title || !message) {
    return res.status(400).json({
      error: &quot;userId, title, and message are required&quot;,
    });
  }

  const notification = {
    id: crypto.randomUUID(),
    title,
    message,
    createdAt: new Date().toISOString(),
    read: false,
  };

  sendNotification(userId, notification);

  res.status(202).json({
    success: true,
    notification,
  });
});

server.listen(3000, () =&amp;gt; {
  console.log(&quot;Server running on port 3000&quot;);
});
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Add the following import near the top of the file if your Node.js environment does not expose &lt;code&gt;crypto&lt;/code&gt; globally:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;const crypto = require(&quot;node:crypto&quot;);
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The example uses an in-memory &lt;code&gt;Map&lt;/code&gt; to track active connections. Each user maps to a set of WebSocket connections, allowing the same account to connect from multiple browser tabs or devices.&lt;/p&gt;
&lt;p&gt;When a connection closes, the server removes it from the set and deletes the user entry if no active connections remain.&lt;/p&gt;
&lt;p&gt;There are two important limitations. First, this example does not persist notifications. Second, the &lt;code&gt;userId&lt;/code&gt; query parameter is not secure authentication. Anyone who can connect and choose an arbitrary user ID could impersonate another recipient.&lt;/p&gt;
&lt;p&gt;We will address both issues in the production design sections below.&lt;/p&gt;
&lt;h3 id=&quot;6-building-the-react-notification-component&quot;&gt;6. Building the React Notification Component&lt;/h3&gt;
&lt;p&gt;Next, create a React application. If you are using Vite, the following commands will get you started.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-bash&quot;&gt;npm create vite@latest client -- --template react
cd client
npm install
npm run dev
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Create a component called &lt;code&gt;NotificationCenter.jsx&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;This component will establish a WebSocket connection, listen for incoming messages, update the notification list, and calculate the unread count.&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useEffect, useState } from &quot;react&quot;;

export default function NotificationCenter({ userId }) {
  const [notifications, setNotifications] = useState([]);
  const [connectionStatus, setConnectionStatus] =
    useState(&quot;connecting&quot;);

  useEffect(() =&amp;gt; {
    if (!userId) return;

    const socket = new WebSocket(
      `ws://localhost:3000/ws?userId=${encodeURIComponent(userId)}`
    );

    socket.addEventListener(&quot;open&quot;, () =&amp;gt; {
      setConnectionStatus(&quot;connected&quot;);
    });

    socket.addEventListener(&quot;message&quot;, (event) =&amp;gt; {
      try {
        const data = JSON.parse(event.data);

        if (data.type !== &quot;notification&quot;) return;

        setNotifications((previous) =&amp;gt; [
          data.payload,
          ...previous,
        ]);
      } catch (error) {
        console.error(&quot;Invalid WebSocket message:&quot;, error);
      }
    });

    socket.addEventListener(&quot;error&quot;, () =&amp;gt; {
      setConnectionStatus(&quot;error&quot;);
    });

    socket.addEventListener(&quot;close&quot;, () =&amp;gt; {
      setConnectionStatus(&quot;disconnected&quot;);
    });

    return () =&amp;gt; {
      socket.close();
    };
  }, [userId]);

  const unreadCount = notifications.filter(
    (notification) =&amp;gt; !notification.read
  ).length;

  function markAsRead(id) {
    setNotifications((previous) =&amp;gt;
      previous.map((notification) =&amp;gt;
        notification.id === id
          ? { ...notification, read: true }
          : notification
      )
    );
  }

  return (
    &amp;lt;section className=&quot;notification-center&quot;&amp;gt;
      &amp;lt;header&amp;gt;
        &amp;lt;h2&amp;gt;Notifications&amp;lt;/h2&amp;gt;
        &amp;lt;span&amp;gt;Unread: {unreadCount}&amp;lt;/span&amp;gt;
      &amp;lt;/header&amp;gt;

      &amp;lt;p&amp;gt;Connection: {connectionStatus}&amp;lt;/p&amp;gt;

      {notifications.length === 0 ? (
        &amp;lt;p&amp;gt;No notifications yet.&amp;lt;/p&amp;gt;
      ) : (
        &amp;lt;ul&amp;gt;
          {notifications.map((notification) =&amp;gt; (
            &amp;lt;li key={notification.id}&amp;gt;
              &amp;lt;strong&amp;gt;{notification.title}&amp;lt;/strong&amp;gt;
              &amp;lt;p&amp;gt;{notification.message}&amp;lt;/p&amp;gt;

              {!notification.read &amp;amp;&amp;amp; (
                &amp;lt;button
                  onClick={() =&amp;gt; markAsRead(notification.id)}
                &amp;gt;
                  Mark as read
                &amp;lt;/button&amp;gt;
              )}
            &amp;lt;/li&amp;gt;
          ))}
        &amp;lt;/ul&amp;gt;
      )}
    &amp;lt;/section&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Render the component from &lt;code&gt;App.jsx&lt;/code&gt;:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import NotificationCenter from &quot;./NotificationCenter&quot;;

export default function App() {
  return (
    &amp;lt;main&amp;gt;
      &amp;lt;h1&amp;gt;Dashboard&amp;lt;/h1&amp;gt;
      &amp;lt;NotificationCenter userId=&quot;user-123&quot; /&amp;gt;
    &amp;lt;/main&amp;gt;
  );
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The hard-coded user ID is only for local demonstration. A real application should obtain the authenticated user identity from its session or authentication provider.&lt;/p&gt;
&lt;p&gt;The React effect creates the connection when the component mounts and closes it when the component unmounts or the &lt;code&gt;userId&lt;/code&gt; changes. This cleanup prevents an obsolete connection from continuing to receive messages.&lt;/p&gt;
&lt;p&gt;React state is updated using the previous state value. This pattern avoids losing notifications when multiple events arrive close together.&lt;/p&gt;
&lt;p&gt;The component also parses incoming messages inside a &lt;code&gt;try...catch&lt;/code&gt; block. A malformed message should not crash the notification interface.&lt;/p&gt;
&lt;h3 id=&quot;7-adding-a-reconnection-strategy&quot;&gt;7. Adding a Reconnection Strategy&lt;/h3&gt;
&lt;p&gt;Network interruptions are inevitable. Users switch between Wi-Fi and mobile data, laptops enter sleep mode, and servers may restart during deployments. A production notification system must recover from these interruptions.&lt;/p&gt;
&lt;p&gt;The initial React component detects when the connection closes, but it does not reconnect automatically. We can improve it with a retry strategy that waits progressively longer between attempts.&lt;/p&gt;
&lt;p&gt;A simple exponential backoff strategy calculates the delay using the number of failed attempts:&lt;/p&gt;
&lt;p&gt;\[
delay = \min(baseDelay \times 2^{attempt}, maxDelay)
\]&lt;/p&gt;
&lt;p&gt;Random jitter can be added to prevent thousands of clients from reconnecting simultaneously after a server outage.&lt;/p&gt;
&lt;p&gt;Here is a reusable React hook demonstrating this approach:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-jsx&quot;&gt;import { useEffect, useState } from &quot;react&quot;;

export function useNotificationSocket(userId) {
  const [status, setStatus] = useState(&quot;disconnected&quot;);
  const [messages, setMessages] = useState([]);

  useEffect(() =&amp;gt; {
    if (!userId) return;

    let socket;
    let retryTimer;
    let attempts = 0;
    let stopped = false;

    function connect() {
      if (stopped) return;

      setStatus(&quot;connecting&quot;);

      socket = new WebSocket(
        `ws://localhost:3000/ws?userId=${encodeURIComponent(userId)}`
      );

      socket.onopen = () =&amp;gt; {
        attempts = 0;
        setStatus(&quot;connected&quot;);
      };

      socket.onmessage = (event) =&amp;gt; {
        try {
          const data = JSON.parse(event.data);

          if (data.type === &quot;notification&quot;) {
            setMessages((previous) =&amp;gt; [
              data.payload,
              ...previous,
            ]);
          }
        } catch {
          console.error(&quot;Unable to parse socket message&quot;);
        }
      };

      socket.onerror = () =&amp;gt; {
        setStatus(&quot;error&quot;);
      };

      socket.onclose = () =&amp;gt; {
        if (stopped) return;

        setStatus(&quot;disconnected&quot;);

        const delay = Math.min(
          1000 * 2 ** attempts,
          30000
        );

        const jitter = Math.random() * 1000;
        attempts += 1;

        retryTimer = setTimeout(
          connect,
          delay + jitter
        );
      };
    }

    connect();

    return () =&amp;gt; {
      stopped = true;
      clearTimeout(retryTimer);

      if (socket) {
        socket.close();
      }
    };
  }, [userId]);

  return { status, messages };
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This hook reconnects after an unexpected disconnection and stops retrying when the component is cleaned up.&lt;/p&gt;
&lt;p&gt;The retry delay starts around one second and increases up to approximately thirty seconds, with additional jitter. In a more advanced implementation, retry attempts can be reset after the connection remains healthy for a specified period.&lt;/p&gt;
&lt;p&gt;The browser's native WebSocket API does not provide automatic reconnection. Developers must implement it themselves or use a library that offers this feature. See the &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebSocket&quot;&gt;MDN WebSocket guide&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Important:&lt;/strong&gt; Reconnection alone does not guarantee that every notification will be received. The application must retrieve missed notifications from persistent storage after reconnecting.&lt;/p&gt;
&lt;h3 id=&quot;8-persisting-notifications-in-a-database&quot;&gt;8. Persisting Notifications in a Database&lt;/h3&gt;
&lt;p&gt;An in-memory notification list is useful for demonstrating the interface, but it has an important limitation: data disappears when the browser reloads or the server restarts.&lt;/p&gt;
&lt;p&gt;A real application should store notifications in a database.&lt;/p&gt;
&lt;p&gt;For example, a PostgreSQL table might look like this:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-sql&quot;&gt;CREATE TABLE notifications (
    id UUID PRIMARY KEY,
    recipient_id UUID NOT NULL,
    title VARCHAR(200) NOT NULL,
    message TEXT NOT NULL,
    event_type VARCHAR(100),
    resource_id UUID,
    created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
    read_at TIMESTAMPTZ
);

CREATE INDEX idx_notifications_recipient_created
ON notifications (recipient_id, created_at DESC);

CREATE INDEX idx_notifications_unread
ON notifications (recipient_id, created_at DESC)
WHERE read_at IS NULL;
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Each notification has a unique identifier, a recipient, a title, a message, a creation timestamp, and an optional read timestamp. The &lt;code&gt;resource_id&lt;/code&gt; field can connect the notification to an order, project, conversation, or other application record.&lt;/p&gt;
&lt;p&gt;The recipient index makes it easier to retrieve notifications for an individual user. The partial index can improve queries that retrieve only unread notifications.&lt;/p&gt;
&lt;p&gt;When a business event occurs, the backend should create the notification record before attempting delivery.&lt;/p&gt;
&lt;p&gt;A typical flow is:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Validate and process the business action.&lt;/li&gt;
&lt;li&gt;Store the business data.&lt;/li&gt;
&lt;li&gt;Create the notification record.&lt;/li&gt;
&lt;li&gt;Publish a notification event.&lt;/li&gt;
&lt;li&gt;Deliver the event to connected clients.&lt;/li&gt;
&lt;li&gt;Allow the client to retrieve stored notifications later.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For important workflows, a transactional outbox pattern can help prevent the database record and event publication from becoming inconsistent. The application writes the business change and an outbox event in the same database transaction, then a background worker publishes pending events.&lt;/p&gt;
&lt;p&gt;This approach helps avoid situations where a transaction succeeds but the notification event is never published because the process crashes.&lt;/p&gt;
&lt;h3 id=&quot;9-loading-existing-notifications-through-an-http-api&quot;&gt;9. Loading Existing Notifications Through an HTTP API&lt;/h3&gt;
&lt;p&gt;WebSockets should handle live updates, while HTTP endpoints can retrieve notification history and manage read states.&lt;/p&gt;
&lt;p&gt;For example, an API could provide these endpoints:&lt;/p&gt;
&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;&lt;th&gt;Method &lt;/th&gt;&lt;th&gt;Endpoint &lt;/th&gt;&lt;th&gt;Purpose&lt;/th&gt;&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;&lt;td&gt;GET &lt;/td&gt;&lt;td&gt;&lt;code&gt;/api/notifications&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Retrieve paginated notifications&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;GET &lt;/td&gt;&lt;td&gt;&lt;code&gt;/api/notifications/unread-count&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Retrieve the unread count&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;PATCH &lt;/td&gt;&lt;td&gt;&lt;code&gt;/api/notifications/:id/read&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Mark a notification as read&lt;/td&gt;&lt;/tr&gt;
&lt;tr&gt;&lt;td&gt;PATCH &lt;/td&gt;&lt;td&gt;&lt;code&gt;/api/notifications/read-all&lt;/code&gt; &lt;/td&gt;&lt;td&gt;Mark all notifications as read&lt;/td&gt;&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;
&lt;p&gt;Every endpoint must derive the recipient from the authenticated session or verified access token. It must never trust a user ID supplied by the request as proof of ownership.&lt;/p&gt;
&lt;p&gt;When React loads the notification center, it should first retrieve recent notifications through the HTTP API. It can then listen for incoming WebSocket events.&lt;/p&gt;
&lt;p&gt;Because a notification could arrive while the initial HTTP request is running, the frontend should merge the results using notification IDs rather than blindly replacing its entire list.&lt;/p&gt;
&lt;p&gt;The same principle applies when the user reconnects after being offline: fetch notifications created after the last successfully processed cursor or timestamp.&lt;/p&gt;
&lt;p&gt;A timestamp-only cursor may be insufficient when several events share the same timestamp. A stable, unique ordering key or event sequence is preferable for reliable pagination.&lt;/p&gt;
&lt;h3 id=&quot;10-implementing-read-and-unread-states-correctly&quot;&gt;10. Implementing Read and Unread States Correctly&lt;/h3&gt;
&lt;p&gt;Unread counters are among the most visible features of a notification system. However, they can become inaccurate if read state exists only in React memory.&lt;/p&gt;
&lt;p&gt;Consider a user who marks a notification as read on a laptop and later opens the application on a mobile device. Both devices should display the same read state.&lt;/p&gt;
&lt;p&gt;The backend should therefore own the authoritative notification status.&lt;/p&gt;
&lt;p&gt;When the user selects &lt;strong&gt;Mark as read&lt;/strong&gt;, React sends a request to the API. The server verifies that the notification belongs to the authenticated user, updates &lt;code&gt;read_at&lt;/code&gt;, and returns the updated record.&lt;/p&gt;
&lt;p&gt;The frontend can use optimistic updates to make the interface feel immediate. If the request fails, it should revert the local change or display a retry option.&lt;/p&gt;
&lt;p&gt;For a system with several connected devices, the server can also broadcast a &lt;code&gt;notification.read&lt;/code&gt; event to the user's other active connections.&lt;/p&gt;
&lt;p&gt;A useful event structure is:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;type&quot;: &quot;notification.read&quot;,
  &quot;payload&quot;: {
    &quot;notificationId&quot;: &quot;notification-uuid&quot;,
    &quot;readAt&quot;: &quot;2026-10-20T09:00:00Z&quot;
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The server must validate every read operation. A notification identifier is not a permission token, and knowing another user's notification ID must not grant access to it.&lt;/p&gt;
&lt;p&gt;For bulk updates, use a database transaction where appropriate and return the authoritative unread count or enough information for the client to calculate it consistently.&lt;/p&gt;
&lt;h3 id=&quot;11-defining-a-consistent-event-protocol&quot;&gt;11. Defining a Consistent Event Protocol&lt;/h3&gt;
&lt;p&gt;As an application grows, sending arbitrary JSON objects through WebSockets becomes difficult to maintain. Establishing a consistent message format makes the system easier to test and extend.&lt;/p&gt;
&lt;p&gt;A notification event could follow this structure:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;{
  &quot;type&quot;: &quot;notification.created&quot;,
  &quot;eventId&quot;: &quot;event-uuid&quot;,
  &quot;version&quot;: 1,
  &quot;occurredAt&quot;: &quot;2026-10-20T09:00:00Z&quot;,
  &quot;payload&quot;: {
    &quot;id&quot;: &quot;notification-uuid&quot;,
    &quot;title&quot;: &quot;New task assigned&quot;,
    &quot;message&quot;: &quot;A task has been assigned to you.&quot;,
    &quot;createdAt&quot;: &quot;2026-10-20T09:00:00Z&quot;,
    &quot;read&quot;: false
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;type&lt;/code&gt; field tells the client which operation to perform. The &lt;code&gt;eventId&lt;/code&gt; helps with tracing and deduplication. The &lt;code&gt;version&lt;/code&gt; field supports gradual protocol changes.&lt;/p&gt;
&lt;p&gt;A client-side event handler can route messages by type:&lt;/p&gt;
&lt;pre&gt;&lt;code class=&quot;language-javascript&quot;&gt;function handleNotificationEvent(event) {
  switch (event.type) {
    case &quot;notification.created&quot;:
      // Add the new notification.
      break;

    case &quot;notification.read&quot;:
      // Update the notification's read state.
      break;

    case &quot;notification.deleted&quot;:
      // Remove the notification if permitted.
      break;

    default:
      console.warn(&quot;Unknown event type:&quot;, event.type);
  }
}
&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In production, validate incoming data against a schema and reject malformed or unsupported messages. Avoid rendering arbitrary server-provided HTML as notification content.&lt;/p&gt;
&lt;p&gt;Stable event contracts also make it easier to introduce mobile applications, additional backend services, and alternative delivery channels without redesigning the entire system.&lt;/p&gt;
&lt;h3 id=&quot;12-securing-websocket-connections&quot;&gt;12. Securing WebSocket Connections&lt;/h3&gt;
&lt;p&gt;Security must be part of the architecture from the beginning.&lt;/p&gt;
&lt;h4 id=&quot;use-secure-transport&quot;&gt;Use secure transport&lt;/h4&gt;
&lt;p&gt;Production applications should use &lt;code&gt;wss://&lt;/code&gt; for encrypted WebSocket connections. When a website is served over HTTPS, browsers generally block insecure WebSocket connections from that page.&lt;/p&gt;
&lt;h4 id=&quot;authenticate-users&quot;&gt;Authenticate users&lt;/h4&gt;
&lt;p&gt;The server must authenticate each connection using a verified session or token. It should derive the user identity from trusted authentication information rather than accepting an arbitrary &lt;code&gt;userId&lt;/code&gt; query parameter.&lt;/p&gt;
&lt;p&gt;Query parameters can appear in logs and monitoring systems. Avoid placing long-lived secrets in WebSocket URLs. Depending on the architecture, a secure session cookie, a short-lived single-use connection ticket, or an appropriate authentication handshake may be preferable.&lt;/p&gt;
&lt;h4 id=&quot;authorize-every-event&quot;&gt;Authorize every event&lt;/h4&gt;
&lt;p&gt;Authentication establishes who the user is; authorization determines what they may access. The server must ensure that a notification is delivered only to its intended recipient and that incoming actions affect only resources the user is allowed to access.&lt;/p&gt;
&lt;h4 id=&quot;validate-origins&quot;&gt;Validate origins&lt;/h4&gt;
&lt;p&gt;Validate the &lt;code&gt;Origin&lt;/code&gt; header against an explicit allowlist when serving browser clients. Origin checks are particularly important when cookies authenticate connections, but they are not a replacement for authentication.&lt;/p&gt;
&lt;h4 id=&quot;validate-payloads-and-limit-message-sizes&quot;&gt;Validate payloads and limit message sizes&lt;/h4&gt;
&lt;p&gt;Reject malformed JSON, unexpected event types, oversized messages, and unsupported operations. Apply rate limits to connection attempts and incoming messages.&lt;/p&gt;
&lt;h4 id=&quot;protect-sensitive-information&quot;&gt;Protect sensitive information&lt;/h4&gt;
&lt;p&gt;Notifications should contain only the information necessary for the user to understand the event. Sensitive financial, personal, or account data should be retrieved through appropriately authorized API endpoints.&lt;/p&gt;
&lt;h4 id=&quot;plan-for-expired-sessions&quot;&gt;Plan for expired sessions&lt;/h4&gt;
&lt;p&gt;If a user's session expires or access is revoked, close or reauthorize existing WebSocket connections. A connection that was valid when first opened should not remain trusted indefinitely.&lt;/p&gt;
&lt;p&gt;The &lt;a rel=&quot;nofollow&quot; href=&quot;https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_client_applications&quot;&gt;MDN WebSocket client guide&lt;/a&gt; provides additional guidance on connection handling and security considerations.&lt;/p&gt;
&lt;h3 id=&quot;13-scaling-beyond-a-single-server&quot;&gt;13. Scaling Beyond a Single Server&lt;/h3&gt;
&lt;p&gt;The in-memory &lt;code&gt;Map&lt;/code&gt; used in our demonstration works for one server process. However, it creates a problem when the application runs across multiple instances.&lt;/p&gt;
&lt;p&gt;Imagine that a user connects to server A, while an event is processed by server B. Server B cannot see server A's local connection map. Without additional coordination, the notification may never reach the connected client.&lt;/p&gt;
&lt;p&gt;A distributed architecture can solve this problem using a message broker such as Redis Pub/Sub or a durable event streaming platform.&lt;/p&gt;
&lt;p&gt;The flow becomes:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;An application service creates a notification.&lt;/li&gt;
&lt;li&gt;The notification is persisted in the database.&lt;/li&gt;
&lt;li&gt;An event is published to a broker.&lt;/li&gt;
&lt;li&gt;WebSocket server instances subscribe to relevant events.&lt;/li&gt;
&lt;li&gt;The instance holding the recipient's connection delivers the notification.&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;For larger systems, a broker with durable retention and consumer acknowledgments can help with reliable processing. Redis Pub/Sub provides lightweight message distribution but does not itself guarantee that disconnected subscribers will receive missed messages.&lt;/p&gt;
&lt;p&gt;Regardless of the broker, the database remains the source of truth for notification history.&lt;/p&gt;
&lt;p&gt;Horizontal scaling also requires attention to connection limits, memory usage, heartbeat handling, reverse-proxy timeouts, and load-balancer configuration. WebSocket connections are long-lived, so infrastructure must be configured to support them.&lt;/p&gt;
&lt;p&gt;Sticky sessions may be needed in some architectures, but they do not replace cross-instance event distribution.&lt;/p&gt;
&lt;h3 id=&quot;14-handling-duplicate-and-missing-notifications&quot;&gt;14. Handling Duplicate and Missing Notifications&lt;/h3&gt;
&lt;p&gt;Networks and distributed systems can deliver the same event more than once. Events may also be delayed, arrive out of order, or be missed during disconnections.&lt;/p&gt;
&lt;p&gt;A robust system should be designed around these possibilities.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Deduplication:&lt;/strong&gt; Store or track processed notification IDs and ignore duplicates when merging the list.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Persistence:&lt;/strong&gt; Save notifications before delivery so that reconnecting clients can retrieve them.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Catch-up synchronization:&lt;/strong&gt; After reconnecting, request notifications created since the last confirmed cursor.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ordering:&lt;/strong&gt; Use stable event identifiers and sequence numbers when ordering is important.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Acknowledgments:&lt;/strong&gt; For critical workflows, define an acknowledgment protocol and retry policy. A WebSocket &lt;code&gt;send()&lt;/code&gt; call alone does not prove that the recipient processed or displayed the notification.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Idempotency:&lt;/strong&gt; Make repeated operations safe. For example, marking an already-read notification as read should not create an inconsistent state.&lt;/p&gt;
&lt;p&gt;The appropriate delivery guarantee depends on the product. A casual social reaction may tolerate a small delay, while a financial transaction alert requires stronger persistence, auditability, and recovery behavior.&lt;/p&gt;
&lt;h3 id=&quot;15-improving-frontend-performance&quot;&gt;15. Improving Frontend Performance&lt;/h3&gt;
&lt;p&gt;A notification center may eventually receive hundreds or thousands of events. React rendering and memory usage should be considered as early as possible.&lt;/p&gt;
&lt;p&gt;Useful optimization techniques include:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Limit the number of notifications kept in active component state.&lt;/li&gt;
&lt;li&gt;Retrieve older notifications through pagination.&lt;/li&gt;
&lt;li&gt;Virtualize long lists to avoid rendering every row simultaneously.&lt;/li&gt;
&lt;li&gt;Batch related state updates where appropriate.&lt;/li&gt;
&lt;li&gt;Use stable notification IDs as React keys.&lt;/li&gt;
&lt;li&gt;Avoid creating a separate WebSocket connection for every notification component.&lt;/li&gt;
&lt;li&gt;Deduplicate events before adding them to state.&lt;/li&gt;
&lt;li&gt;Keep notification rendering lightweight.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;The standard browser WebSocket API does not provide automatic backpressure. If messages arrive faster than the application can process them, memory usage and responsiveness may suffer. The MDN WebSocket documentation discusses this limitation.&lt;/p&gt;
&lt;p&gt;For high-volume systems, consider server-side filtering, bounded client queues, event batching, message coalescing, and flow-control strategies.&lt;/p&gt;
&lt;p&gt;For example, an application dashboard may not need to render every intermediate progress update. It can keep only the latest progress value while preserving important events in persistent storage.&lt;/p&gt;
&lt;h3 id=&quot;16-testing-the-notification-system&quot;&gt;16. Testing the Notification System&lt;/h3&gt;
&lt;p&gt;Testing should cover both the expected workflow and failure scenarios.&lt;/p&gt;
&lt;h4 id=&quot;unit-testing&quot;&gt;Unit testing&lt;/h4&gt;
&lt;p&gt;Test notification reducers, event parsers, deduplication logic, unread counts, and read-state updates independently of the WebSocket server.&lt;/p&gt;
&lt;h4 id=&quot;integration-testing&quot;&gt;Integration testing&lt;/h4&gt;
&lt;p&gt;Start the backend and connect a test client. Verify that a notification created for user A reaches user A but not user B.&lt;/p&gt;
&lt;h4 id=&quot;reconnection-testing&quot;&gt;Reconnection testing&lt;/h4&gt;
&lt;p&gt;Disconnect the client, create notifications, reconnect, and confirm that the HTTP catch-up process retrieves missed events.&lt;/p&gt;
&lt;h4 id=&quot;authorization-testing&quot;&gt;Authorization testing&lt;/h4&gt;
&lt;p&gt;Attempt to read another user's notification, mark another user's notification as read, and connect without valid authentication. All such attempts should be rejected.&lt;/p&gt;
&lt;h4 id=&quot;load-testing&quot;&gt;Load testing&lt;/h4&gt;
&lt;p&gt;Simulate many concurrent connections and bursts of events. Measure delivery latency, CPU utilization, memory consumption, connection failures, and database performance.&lt;/p&gt;
&lt;h4 id=&quot;browser-testing&quot;&gt;Browser testing&lt;/h4&gt;
&lt;p&gt;Verify behavior during navigation, page refreshes, temporary network failures, session expiration, and multiple browser tabs.&lt;/p&gt;
&lt;p&gt;React's development Strict Mode may run an additional setup-and-cleanup cycle for effects. Ensure that socket cleanup is correct and that the application does not accumulate unnecessary connections during development.&lt;/p&gt;
&lt;h3 id=&quot;17-monitoring-and-production-deployment&quot;&gt;17. Monitoring and Production Deployment&lt;/h3&gt;
&lt;p&gt;A system that works locally may fail under real traffic unless it is monitored and deployed correctly.&lt;/p&gt;
&lt;p&gt;Track metrics such as:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Active WebSocket connections.&lt;/li&gt;
&lt;li&gt;Successful and failed connection attempts.&lt;/li&gt;
&lt;li&gt;Notification delivery latency.&lt;/li&gt;
&lt;li&gt;Reconnection frequency.&lt;/li&gt;
&lt;li&gt;Broker queue depth and processing delays.&lt;/li&gt;
&lt;li&gt;Database query duration.&lt;/li&gt;
&lt;li&gt;Invalid messages and authorization failures.&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;Use structured logs and correlation IDs to trace a notification from the original business event to database persistence, broker publication, WebSocket delivery, and client acknowledgment.&lt;/p&gt;
&lt;p&gt;Do not log authentication tokens or sensitive notification payloads.&lt;/p&gt;
&lt;p&gt;For deployment, configure a reverse proxy or load balancer to support WebSocket upgrades and long-lived connections. Ensure that proxy timeouts are appropriate, and implement heartbeat or ping/pong mechanisms to detect stale connections.&lt;/p&gt;
&lt;p&gt;Configure environment variables for service URLs, authentication settings, and database credentials. Secrets should be managed through an appropriate secrets-management system rather than committed to source control.&lt;/p&gt;
&lt;p&gt;Finally, use health checks and graceful shutdown procedures. When a server instance is about to stop, it should stop accepting new connections, notify clients when possible, close existing connections cleanly, and allow clients to reconnect to a healthy instance.&lt;/p&gt;
&lt;h3 id=&quot;18-common-mistakes-to-avoid&quot;&gt;18. Common Mistakes to Avoid&lt;/h3&gt;
&lt;p&gt;Several implementation mistakes can undermine a real-time notification system.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Using WebSockets without persistence:&lt;/strong&gt; Users may lose access to important events after refreshing the browser.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Trusting client-provided user IDs:&lt;/strong&gt; This can expose notifications to unauthorized users.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ignoring cleanup:&lt;/strong&gt; Old connections may remain active after a React component unmounts or its user changes.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Assuming delivery is guaranteed:&lt;/strong&gt; A successful connection does not guarantee that every event was received and processed.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Creating too many connections:&lt;/strong&gt; Multiple components opening independent sockets can waste server resources.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Skipping reconnect synchronization:&lt;/strong&gt; Reconnecting without fetching missed events leaves gaps in notification history.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Ignoring cross-instance delivery:&lt;/strong&gt; Local in-memory connection tracking is insufficient for a multi-server deployment.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Sending sensitive information through notification payloads:&lt;/strong&gt; Excessive data increases the impact of accidental exposure.&lt;/p&gt;
&lt;p&gt;Addressing these issues early can prevent expensive architectural changes later.&lt;/p&gt;
&lt;h3 id=&quot;conclusion&quot;&gt;Conclusion&lt;/h3&gt;
&lt;p&gt;Building a real-time notification system with React and WebSockets is an excellent way to improve the responsiveness of modern web applications. WebSockets provide a persistent communication channel, while React makes it straightforward to render incoming events and update notification counts.&lt;/p&gt;
&lt;p&gt;However, a production-ready solution requires more than opening a socket and displaying a message. It needs authenticated connections, authorization checks, persistent notification records, reconnection handling, duplicate protection, missed-event recovery, and a scalable event-distribution architecture.&lt;/p&gt;
&lt;p&gt;A practical development path is to begin with a single WebSocket server and React notification component, introduce database persistence, add HTTP endpoints for notification history and read states, and then implement distributed messaging when scaling demands it.&lt;/p&gt;
&lt;p&gt;By combining these practices, developers can build notification systems that remain responsive, secure, maintainable, and reliable as an application grows.&lt;/p&gt;
&lt;p&gt;Whether you are developing a project management dashboard, an e-commerce platform, a customer relationship management system, or a collaborative workspace, the same core principles apply: persist important events, deliver them efficiently, authorize every operation, and design for interruptions from the beginning.&lt;/p&gt;
</description>
<category>Articles</category>
<guid isPermaLink="true">https://coderlegion.com/31801/building-a-real-time-notification-system-with-react-and-websockets</guid>
<pubDate>Tue, 20 Oct 2026 03:24:57 +0000</pubDate>
</item>
</channel>
</rss>