Back to Blog
IoT Dashboard

Building Real-Time IoT Dashboards with React and Recharts

Recharts is the most popular React charting library, but making it work with live IoT data requires careful state management, smart data windowing, and preventing re-render storms. Here's the production architecture we use.

September 22, 2024
12 min read
ReactRechartsIoT DashboardWebSocket

Building Real-Time IoT Dashboards with React and Recharts

React dashboards that poll REST APIs every second are the wrong approach for IoT. You get stale data, unnecessary server load, and poor UX. The right stack is WebSocket for real-time push, Recharts for visualization, and careful state management to prevent re-render hell.

This guide builds a production-grade IoT dashboard that handles 50+ devices updating every second without the UI becoming a slideshow.

Architecture

IoT Devices → MQTT Broker → Node.js WebSocket Server
                                      ↓
                              React Dashboard
                              ├── useWebSocket hook
                              ├── useReducer (telemetry store)
                              └── Recharts (charts)

Step 1: WebSocket Hook

// hooks/useWebSocket.ts
import { useEffect, useRef, useCallback } from 'react'

interface WebSocketHookOptions { url: string onMessage: (data: unknown) => void onConnect?: () => void onDisconnect?: () => void }

export function useWebSocket({ url, onMessage, onConnect, onDisconnect }: WebSocketHookOptions) { const ws = useRef<WebSocket | null>(null) const reconnectTimer = useRef<ReturnType<typeof setTimeout>>() const attempts = useRef(0)

const connect = useCallback(() => { ws.current = new WebSocket(url)

ws.current.onopen = () => { attempts.current = 0 onConnect?.() }

ws.current.onmessage = (event) => { try { const data = JSON.parse(event.data) onMessage(data) } catch {} }

ws.current.onclose = () => { onDisconnect?.() const delay = Math.min(30000, 1000 * 2 ** attempts.current) attempts.current++ reconnectTimer.current = setTimeout(connect, delay) }

ws.current.onerror = () => ws.current?.close() }, [url, onMessage, onConnect, onDisconnect])

useEffect(() => { connect() return () => { clearTimeout(reconnectTimer.current) ws.current?.close() } }, [connect])

const send = useCallback((data: unknown) => { if (ws.current?.readyState === WebSocket.OPEN) { ws.current.send(JSON.stringify(data)) } }, [])

return { send } }

Step 2: Telemetry State with useReducer

Never use useState for accumulating IoT data — useReducer gives you predictable updates and prevents stale closures:

// store/telemetry.ts
export interface DataPoint {
  ts:          number
  temperature: number
  humidity:    number
  voltage:     number
}

export interface DeviceTelemetry { deviceId: string label: string isOnline: boolean history: DataPoint[] // Rolling 60-point window latest: DataPoint | null }

type Action = | { type: 'UPDATE_DEVICE'; deviceId: string; point: DataPoint } | { type: 'SET_OFFLINE'; deviceId: string } | { type: 'INIT_DEVICES'; devices: { deviceId: string; label: string }[] }

const HISTORY_WINDOW = 60 // Last 60 data points per device

export function telemetryReducer( state: Record<string, DeviceTelemetry>, action: Action, ): Record<string, DeviceTelemetry> { switch (action.type) { case 'UPDATE_DEVICE': { const existing = state[action.deviceId] if (!existing) return state

const newHistory = [...existing.history, action.point].slice(-HISTORY_WINDOW) return { ...state, [action.deviceId]: { ...existing, isOnline: true, latest: action.point, history: newHistory, }, } } case 'SET_OFFLINE': return { ...state, [action.deviceId]: { ...state[action.deviceId], isOnline: false }, } case 'INIT_DEVICES': { const init: Record<string, DeviceTelemetry> = {} for (const d of action.devices) { init[d.deviceId] = { ...d, isOnline: false, history: [], latest: null } } return init } } }

Step 3: Dashboard Component

// Dashboard.tsx
import { useReducer, useCallback } from 'react'
import { telemetryReducer } from './store/telemetry'
import { useWebSocket } from './hooks/useWebSocket'
import { DevicePanel } from './DevicePanel'

export function Dashboard() { const [devices, dispatch] = useReducer(telemetryReducer, {})

const handleMessage = useCallback((data: unknown) => { const msg = data as { type: string; deviceId: string; payload: any }

if (msg.type === 'telemetry') { dispatch({ type: 'UPDATE_DEVICE', deviceId: msg.deviceId, point: { ts: Date.now(), temperature: msg.payload.temperature, humidity: msg.payload.humidity, voltage: msg.payload.voltage, }, }) }

if (msg.type === 'device_offline') { dispatch({ type: 'SET_OFFLINE', deviceId: msg.deviceId }) } }, [])

useWebSocket({ url: `${process.env.REACT_APP_WS_URL}/dashboard`, onMessage: handleMessage, })

return ( <div className="grid grid-cols-1 md:grid-cols-2 xl:grid-cols-3 gap-4 p-4"> {Object.values(devices).map(device => ( <DevicePanel key={device.deviceId} device={device} /> ))} </div> ) }

Step 4: Recharts Live Chart

// DevicePanel.tsx
import { memo } from 'react'
import {
  LineChart, Line, XAxis, YAxis, CartesianGrid,
  Tooltip, ResponsiveContainer
} from 'recharts'
import type { DeviceTelemetry } from './store/telemetry'

interface Props { device: DeviceTelemetry }

// memo() prevents re-render when OTHER devices update export const DevicePanel = memo(function DevicePanel({ device }: Props) { const chartData = device.history.map((pt, i) => ({ index: i, temperature: pt.temperature, humidity: pt.humidity, ts: new Date(pt.ts).toLocaleTimeString(), }))

return ( <div className="bg-gray-900 rounded-xl p-4 border border-gray-700"> <div className="flex items-center justify-between mb-3"> <h3 className="text-white font-semibold">{device.label}</h3> <span className={device.isOnline ? 'text-green-400' : 'text-red-400'}> {device.isOnline ? '● Online' : '○ Offline'} </span> </div>

{device.latest && ( <div className="grid grid-cols-3 gap-2 mb-4"> <Stat label="Temp" value={`${device.latest.temperature.toFixed(1)}°C`} color="text-orange-400" /> <Stat label="Humidity" value={`${device.latest.humidity.toFixed(0)}%`} color="text-blue-400" /> <Stat label="Voltage" value={`${device.latest.voltage.toFixed(2)}V`} color="text-green-400" /> </div> )}

<ResponsiveContainer width="100%" height={120}> <LineChart data={chartData}> <CartesianGrid strokeDasharray="3 3" stroke="#374151" /> <XAxis dataKey="ts" hide /> <YAxis domain={['auto', 'auto']} width={35} tick={{ fill: '#9CA3AF', fontSize: 11 }} /> <Tooltip contentStyle={{ background: '#111827', border: '1px solid #374151' }} labelStyle={{ color: '#9CA3AF' }} /> <Line type="monotone" dataKey="temperature" stroke="#FF6B35" dot={false} strokeWidth={2} isAnimationActive={false} // Critical: disable for real-time data /> </LineChart> </ResponsiveContainer> </div> ) })

const Stat = ({ label, value, color }: { label: string; value: string; color: string }) => ( <div className="text-center"> <div className={`text-lg font-mono font-bold ${color}`}>{value}</div> <div className="text-gray-500 text-xs">{label}</div> </div> )

Critical performance notes:

  • 1. isAnimationActive={false} — Recharts animations on real-time data cause jank
  • 2. memo() — each DevicePanel only re-renders when its own device data changes
  • 3. useCallback on handleMessage — prevents WebSocket hook from reconnecting on every render
  • Step 5: Performance Optimizations

    For 50+ devices updating every second:

    // Throttle UI updates — don't re-render more than 2×/second per device
    import { throttle } from 'lodash'

    const handleMessage = useCallback( throttle((data: unknown) => { // dispatch updates }, 500), // 500ms = max 2 renders/sec [], )

    // Virtualize the device grid for large fleets import { FixedSizeGrid } from 'react-window'

    // For 100+ devices, use react-window instead of a flat grid

    Need a production IoT dashboard built for your fleet? [Contact Code Caracal](/contact) — we've built dashboards monitoring thousands of real-time devices.

    ⚡
    Written by CodeCaracal Engineering

    We write from production experience — every technique in our articles has been deployed to real clients. No academic theory.

    Let's Build Together

    Got an IoT challenge?
    We've shipped it.

    Whether you need a fleet to track, a factory to monitor, or a farm to automate — our team has done it before and we'd love to build it with you. Typical response time: under 24 hours.

    No upfront commitment99.9% uptime SLANDA on requestFixed-price options