Back to journal
Engineering

Build a Real-Time Dashboard with Server-Sent Events and React

WebSockets are overkill for dashboards. SSE gives you real-time push with zero library dependencies, automatic reconnection, and works through every proxy.

PMML Engineering 30 April 2026 9 min read 0 views
Build a Real-Time Dashboard with Server-Sent Events and React

WebSockets are the default answer for "real-time." But for dashboards — where the server pushes data and the client just renders — they're overkill.

Server-Sent Events (SSE) are simpler, lighter, and built into every browser. No library needed. Automatic reconnection included. Let's build a full real-time metrics dashboard.

Why SSE Over WebSockets for Dashboards

Real-time analytics dashboard with live data streams

| Feature | WebSockets | SSE | |---------|-----------|-----| | Direction | Bidirectional | Server → Client | | Protocol | Custom | HTTP | | Reconnection | Manual | Automatic | | Browser support | All modern | All modern | | Proxy/CDN friendly | Tricky | Works everywhere | | Dependencies | ws library | Zero |

Dashboards are read-only streams. You don't need bidirectional communication. SSE gives you exactly what you need with less complexity.

Part 1: The Server (Node.js)

// server.ts
import http from "http";

type Metric = {
  cpu: number;
  memory: number;
  requests: number;
  errors: number;
  latency: number;
  timestamp: number;
};

function randomMetric(): Metric {
  return {
    cpu: Math.round(20 + Math.random() * 60),
    memory: Math.round(40 + Math.random() * 40),
    requests: Math.round(100 + Math.random() * 900),
    errors: Math.round(Math.random() * 15),
    latency: Math.round(10 + Math.random() * 200),
    timestamp: Date.now(),
  };
}

const clients = new Set<http.ServerResponse>();

function broadcast(data: object) {
  const payload = `data: ${JSON.stringify(data)}\n\n`;
  for (const client of clients) {
    client.write(payload);
  }
}

const server = http.createServer((req, res) => {
  if (req.url === "/events") {
    res.writeHead(200, {
      "Content-Type": "text/event-stream",
      "Cache-Control": "no-cache",
      Connection: "keep-alive",
      "Access-Control-Allow-Origin": "*",
    });

    // Send initial data immediately
    res.write(`data: ${JSON.stringify(randomMetric())}\n\n`);
    clients.add(res);

    req.on("close", () => {
      clients.delete(res);
    });
    return;
  }

  // Serve static for demo
  res.writeHead(200, { "Content-Type": "text/html" });
  res.end("<h1>SSE Dashboard Server</h1><p>Connect to /events</p>");
});

// Push metrics every second
setInterval(() => {
  broadcast(randomMetric());
}, 1000);

server.listen(8080, () => {
  console.log("Dashboard server running on :8080");
});

That's the entire backend — 50 lines. Every connected client gets a JSON metric every second via SSE.

Part 2: React Dashboard Client

Data visualization charts updating in real-time

// Dashboard.tsx
import { useEffect, useRef, useState } from "react";

type Metric = {
  cpu: number;
  memory: number;
  requests: number;
  errors: number;
  latency: number;
  timestamp: number;
};

function useSSE<T>(url: string): T | null {
  const [data, setData] = useState<T | null>(null);

  useEffect(() => {
    const source = new EventSource(url);
    source.onmessage = (event) => {
      setData(JSON.parse(event.data));
    };
    source.onerror = () => {
      // EventSource auto-reconnects — just log it
      console.warn("SSE connection lost, reconnecting...");
    };
    return () => source.close();
  }, [url]);

  return data;
}

function MetricCard({
  label,
  value,
  unit,
  color,
  max = 100,
}: {
  label: string;
  value: number;
  unit: string;
  color: string;
  max?: number;
}) {
  const pct = Math.min(100, (value / max) * 100);
  return (
    <div style={{
      background: "#1a1a2e",
      borderRadius: 12,
      padding: 24,
      minWidth: 200,
    }}>
      <p style={{ color: "#888", fontSize: 12, textTransform: "uppercase" }}>
        {label}
      </p>
      <p style={{ color, fontSize: 36, fontWeight: 700, margin: "8px 0" }}>
        {value.toLocaleString()}
        <span style={{ fontSize: 14, color: "#666" }}> {unit}</span>
      </p>
      <div style={{
        height: 4,
        background: "#2a2a3e",
        borderRadius: 2,
        overflow: "hidden",
      }}>
        <div
          style={{
            height: "100%",
            width: `${pct}%`,
            background: color,
            borderRadius: 2,
            transition: "width 0.3s ease",
          }}
        />
      </div>
    </div>
  );
}

function SparkLine({ data, color }: { data: number[]; color: string }) {
  const max = Math.max(...data, 1);
  const min = Math.min(...data, 0);
  const range = max - min || 1;
  const w = 200;
  const h = 40;

  const points = data
    .map((v, i) => {
      const x = (i / (data.length - 1)) * w;
      const y = h - ((v - min) / range) * h;
      return `${x},${y}`;
    })
    .join(" ");

  return (
    <svg width={w} height={h} style={{ display: "block" }}>
      <polyline
        points={points}
        fill="none"
        stroke={color}
        strokeWidth={2}
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

export default function Dashboard() {
  const metric = useSSE<Metric>("http://localhost:8080/events");
  const historyRef = useRef<Metric[]>([]);

  if (metric) {
    historyRef.current = [...historyRef.current.slice(-59), metric];
  }

  const history = historyRef.current;

  if (!metric) {
    return (
      <div style={{ color: "#888", textAlign: "center", padding: 100 }}>
        Connecting to dashboard...
      </div>
    );
  }

  return (
    <div style={{ background: "#0f0f1a", minHeight: "100vh", padding: 40 }}>
      <h1 style={{ color: "#fff", fontSize: 24, marginBottom: 8 }}>
        System Dashboard
      </h1>
      <p style={{ color: "#666", fontSize: 14, marginBottom: 32 }}>
        Live metrics · updates every second
      </p>

      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: 16, marginBottom: 32 }}>
        <MetricCard label="CPU" value={metric.cpu} unit="%" color="#3B82F6" />
        <MetricCard label="Memory" value={metric.memory} unit="%" color="#10B981" />
        <MetricCard label="Requests" value={metric.requests} unit="/min" color="#F59E0B" max={1000} />
        <MetricCard label="Errors" value={metric.errors} unit="" color="#EF4444" max={20} />
        <MetricCard label="Latency" value={metric.latency} unit="ms" color="#8B5CF6" max={300} />
      </div>

      {history.length > 1 && (
        <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(200px, 1fr))", gap: 16 }}>
          <div style={{ background: "#1a1a2e", borderRadius: 12, padding: 16 }}>
            <p style={{ color: "#888", fontSize: 11, marginBottom: 8 }}>CPU (60s)</p>
            <SparkLine data={history.map((m) => m.cpu)} color="#3B82F6" />
          </div>
          <div style={{ background: "#1a1a2e", borderRadius: 12, padding: 16 }}>
            <p style={{ color: "#888", fontSize: 11, marginBottom: 8 }}>Latency (60s)</p>
            <SparkLine data={history.map((m) => m.latency)} color="#8B5CF6" />
          </div>
          <div style={{ background: "#1a1a2e", borderRadius: 12, padding: 16 }}>
            <p style={{ color: "#888", fontSize: 11, marginBottom: 8 }}>Errors (60s)</p>
            <SparkLine data={history.map((m) => m.errors)} color="#EF4444" />
          </div>
        </div>
      )}
    </div>
  );
}

The useSSE Hook

The entire real-time connection is 15 lines:

function useSSE<T>(url: string): T | null {
  const [data, setData] = useState<T | null>(null);

  useEffect(() => {
    const source = new EventSource(url);
    source.onmessage = (event) => {
      setData(JSON.parse(event.data));
    };
    source.onerror = () => {
      console.warn("SSE connection lost, reconnecting...");
    };
    return () => source.close();
  }, [url]);

  return data;
}

EventSource is a browser built-in. It:

  • Opens an HTTP connection to the server
  • Parses incoming data: events as they arrive
  • Automatically reconnects if the connection drops
  • Can carry a Last-Event-ID header for resuming where you left off

No npm install. No WebSocket library. No reconnection logic.

Exercises

Exercise 1: Named Event Types

Send different event types from the server:

// Server
res.write(`event: metric\ndata: ${JSON.stringify(metric)}\n\n`);
res.write(`event: alert\ndata: ${JSON.stringify({ message: "CPU spike!" })}\n\n`);

// Client
source.addEventListener("metric", (e) => {
  setData(JSON.parse(e.data));
});
source.addEventListener("alert", (e) => {
  showNotification(JSON.parse(e.data).message);
});

Exercise 2: Connection Status Indicator

Show connection state in the UI:

function useSSE<T>(url: string) {
  const [data, setData] = useState<T | null>(null);
  const [status, setStatus] = useState<"connecting" | "open" | "error">("connecting");

  useEffect(() => {
    const source = new EventSource(url);
    source.onopen = () => setStatus("open");
    source.onmessage = (e) => setData(JSON.parse(e.data));
    source.onerror = () => setStatus("error");
    return () => source.close();
  }, [url]);

  return { data, status };
}

Exercise 3: Historical Data with Last-Event-ID

Resume from where you left off after a reconnect:

// Server — include ID with each event
let eventId = 0;
res.write(`id: ${eventId++}\ndata: ${JSON.stringify(metric)}\n\n`);

// On reconnect, browser sends Last-Event-ID header automatically
const lastId = req.headers["last-event-id"];
if (lastId) {
  // Send missed events since lastId
}

When to Use SSE vs WebSockets

Technology comparison decision matrix

Use SSE when:

  • Data flows in one direction (server → client)
  • You need automatic reconnection
  • You're behind corporate proxies or CDNs
  • You want zero dependencies

Use WebSockets when:

  • You need bidirectional communication (chat, games)
  • You need binary data transfer
  • You need custom protocols

For 90% of dashboards, monitoring pages, and live feeds — SSE is the right choice.

What dashboard are you building? Tell us at pmml.info@gmail.com.

PMML Engineering builds real-time systems, web platforms, and AI products for teams that ship. Based in Accra, Ghana.

#react#sse#real-time#dashboard#typescript

Keep reading

You might also like