Skip to main content

Building Real-Time Dashboard React Charts

Real-time dashboards display live data: stock prices update every second, server metrics refresh every minute, sensor readings stream continuously. This article teaches you to build dashboards that stay synchronized with live data using WebSockets, polling, and React state management. You'll learn to handle streaming data without performance degradation, smooth updates gracefully, and keep multiple charts in sync.

Understanding Real-Time Data Sources

Real-time data arrives from several sources:

WebSocket: A bidirectional, low-latency connection. Server pushes updates whenever they occur. Ideal for high-frequency data (trading floors, live sports) where every update matters.

Server-Sent Events (SSE): Server pushes to client via HTTP. Simpler than WebSocket for unidirectional streams. Used for notifications, live feeds.

Polling: Client asks the server periodically (every 5 seconds, minute, etc.). Simple but higher latency; best for non-critical updates.

For 2026 dashboards, most use a hybrid: WebSocket for critical data, polling for supplementary metrics.

Building a Real-Time Chart with Polling

Start simple with polling (fetching data every N seconds):

import React, { useState, useEffect } from "react";
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from "recharts";

export function PollingDashboard() {
const [data, setData] = useState([]);
const [isConnected, setIsConnected] = useState(true);

useEffect(() => {
const fetchMetrics = async () => {
try {
const response = await fetch("/api/metrics");
const newData = await response.json();

// Add timestamp and keep last 60 points
setData((prevData) => {
const updated = [
...prevData,
{ ...newData, timestamp: new Date().toLocaleTimeString() }
];
return updated.slice(-60); // keep last 60 seconds
});

setIsConnected(true);
} catch (error) {
console.error("Polling error:", error);
setIsConnected(false);
}
};

// Poll every 1 second
const interval = setInterval(fetchMetrics, 1000);

// Initial fetch
fetchMetrics();

return () => clearInterval(interval);
}, []);

return (
<div style={{ padding: "20px" }}>
<div style={{ marginBottom: "10px" }}>
<span style={{ marginRight: "10px" }}>
{isConnected ? "● Connected" : "○ Disconnected"}
</span>
<span>Data points: {data.length}</span>
</div>

<ResponsiveContainer width="100%" height={300}>
<LineChart data={data} margin={{ top: 5, right: 30, left: 0, bottom: 5 }}>
<CartesianGrid strokeDasharray="3 3" />
<XAxis
dataKey="timestamp"
tick={{ fontSize: 12 }}
angle={-45}
height={60}
/>
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey="cpuUsage"
stroke="#8884d8"
isAnimationActive={false} // disable animation for smooth streaming
dot={false} // hide dots on high-frequency data
/>
</LineChart>
</ResponsiveContainer>
</div>
);
}

This fetches metrics every second, adds new data, and keeps the last 60 points. isAnimationActive={false} prevents jank on frequent updates. The connection status indicator shows whether the polling is working.

Real-Time Updates with WebSocket

WebSocket is lower-latency and more scalable for frequent updates:

import React, { useState, useEffect } from "react";

export function WebSocketDashboard() {
const [data, setData] = useState([]);
const [wsStatus, setWsStatus] = useState("connecting");

useEffect(() => {
const ws = new WebSocket("wss://your-api.com/metrics");

ws.onopen = () => {
console.log("WebSocket connected");
setWsStatus("connected");
};

ws.onmessage = (event) => {
const newPoint = JSON.parse(event.data);

setData((prevData) => {
// Add new point and trim to last 100
const updated = [
...prevData,
{
...newPoint,
timestamp: new Date().toLocaleTimeString()
}
];
return updated.slice(-100);
});
};

ws.onerror = (error) => {
console.error("WebSocket error:", error);
setWsStatus("error");
};

ws.onclose = () => {
console.log("WebSocket closed");
setWsStatus("disconnected");

// Reconnect after 3 seconds
setTimeout(() => {
// Retry connection
}, 3000);
};

return () => {
if (ws.readyState === WebSocket.OPEN) {
ws.close();
}
};
}, []);

return (
<div>
<p>WebSocket Status: {wsStatus}</p>
{/* Render chart with data */}
</div>
);
}

WebSocket stays open, receiving updates as server sends them. No polling overhead. Reconnect logic handles drops gracefully.

Synchronizing Multiple Real-Time Charts

Dashboards usually show multiple metrics. Keep them in sync with a shared state:

import React, { useState, useEffect } from "react";
import {
LineChart, Line, BarChart, Bar, XAxis, YAxis,
CartesianGrid, Tooltip, ResponsiveContainer
} from "recharts";

export function SyncedDashboard() {
const [metrics, setMetrics] = useState({
cpuUsage: [],
memoryUsage: [],
diskIO: []
});

useEffect(() => {
const pollMetrics = async () => {
try {
const response = await fetch("/api/full-metrics");
const { cpu, memory, disk } = await response.json();

setMetrics((prev) => ({
cpuUsage: [...prev.cpuUsage, { value: cpu, time: new Date().getTime() }].slice(-60),
memoryUsage: [...prev.memoryUsage, { value: memory, time: new Date().getTime() }].slice(-60),
diskIO: [...prev.diskIO, { value: disk, time: new Date().getTime() }].slice(-60)
}));
} catch (error) {
console.error("Polling error:", error);
}
};

const interval = setInterval(pollMetrics, 1000);
pollMetrics();

return () => clearInterval(interval);
}, []);

return (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(400px, 1fr))",
gap: "20px",
padding: "20px"
}}
>
{/* CPU Chart */}
<div>
<h3>CPU Usage</h3>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={metrics.cpuUsage}>
<CartesianGrid />
<XAxis dataKey="time" />
<YAxis domain={[0, 100]} />
<Tooltip />
<Line
type="monotone"
dataKey="value"
stroke="#8884d8"
isAnimationActive={false}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>

{/* Memory Chart */}
<div>
<h3>Memory Usage</h3>
<ResponsiveContainer width="100%" height={250}>
<BarChart data={metrics.memoryUsage}>
<CartesianGrid />
<XAxis dataKey="time" />
<YAxis domain={[0, 100]} />
<Tooltip />
<Bar dataKey="value" fill="#82ca9d" isAnimationActive={false} />
</BarChart>
</ResponsiveContainer>
</div>

{/* Disk I/O Chart */}
<div>
<h3>Disk I/O</h3>
<ResponsiveContainer width="100%" height={250}>
<LineChart data={metrics.diskIO}>
<CartesianGrid />
<XAxis dataKey="time" />
<YAxis />
<Tooltip />
<Line
type="monotone"
dataKey="value"
stroke="#ffc658"
isAnimationActive={false}
dot={false}
/>
</LineChart>
</ResponsiveContainer>
</div>
</div>
);
}

All three charts update from the same metrics object. Fetch all data in one request, split it into series, and render multiple charts. This keeps them synchronized.

Handling Data Overflow and Circular Buffers

Real-time data accumulates quickly. Use a circular buffer (keep only the last N points) to avoid memory leaks:

function useCircularBuffer(maxSize) {
const [buffer, setBuffer] = useState([]);

const add = (item) => {
setBuffer((prev) => {
const updated = [...prev, item];
return updated.length > maxSize ? updated.slice(-maxSize) : updated;
});
};

const clear = () => setBuffer([]);

return { buffer, add, clear };
}

// Usage
export function OptimizedRealTimeChart() {
const { buffer: data, add } = useCircularBuffer(500); // keep last 500 points

useEffect(() => {
const interval = setInterval(() => {
fetch("/api/metric")
.then((r) => r.json())
.then((point) => add(point));
}, 1000);

return () => clearInterval(interval);
}, [add]);

return (
<ResponsiveContainer width="100%" height={300}>
<LineChart data={data}>
{/* ... */}
</LineChart>
</ResponsiveContainer>
);
}

This hook encapsulates the circular buffer pattern. Add points, and old ones automatically drop off.

Smooth Visual Updates with Animations

Real-time data can look jittery. Smooth transitions help:

import { motion } from "framer-motion";

export function SmoothRealtimeMetric({ value, label }) {
return (
<div>
<h4>{label}</h4>
<motion.div
animate={{ opacity: 1 }}
initial={{ opacity: 0.5 }}
transition={{ duration: 0.3 }}
style={{
fontSize: "32px",
fontWeight: "bold",
color: value > 80 ? "red" : "green"
}}
>
{value.toFixed(1)}%
</motion.div>
</div>
);
}

// Use in dashboard
<SmoothRealtimeMetric value={cpuUsage} label="CPU" />

Framer Motion smoothly animates value changes, making the dashboard feel fluid rather than jumpy.

Key Takeaways

  • Real-time dashboards require live data sources: WebSocket for low-latency, polling for simplicity.
  • Store streaming data in a circular buffer (last N points) to avoid memory leaks.
  • Disable animations on high-frequency charts (isAnimationActive={false}) to prevent jank.
  • Synchronize multiple charts with shared state and a single data fetch.
  • Implement graceful disconnection and reconnection logic for reliability.

Frequently Asked Questions

What's the difference between WebSocket and polling for real-time data?

WebSocket is bidirectional and low-latency; server pushes whenever data changes. Polling is unidirectional; client asks periodically. Use WebSocket for high-frequency (stock prices, sensor streams); polling for low-frequency (daily sales, monthly reports).

How many real-time metrics can I display simultaneously?

Depends on update frequency. 5–10 charts updating every second is fine. 50+ charts or updates every 100ms may cause performance issues. Use aggregation or sampling to reduce data volume.

How do I reconnect after a WebSocket drops?

Implement exponential backoff:

const reconnect = (attempt = 0) => {
const delay = Math.min(1000 * Math.pow(2, attempt), 30000); // max 30s
setTimeout(() => {
ws = new WebSocket(url);
// ... set up handlers
}, delay);
};

ws.onclose = () => reconnect();

How do I alert users to anomalies (e.g., CPU > 90%)?

Add threshold detection:

useEffect(() => {
if (cpuUsage > 90) {
console.warn("High CPU usage detected");
// show alert, change color, etc.
}
}, [cpuUsage]);

Can I persist real-time data to a database?

Yes, send data to your backend, which stores it. Users can then query historical data. Use a time-series database (InfluxDB, TimescaleDB) optimized for this pattern.

Further Reading