IoT-Tutorials

React MQTT Tutorial: Ein Hooks-basiertes Echtzeit-Dashboard

Erstellen Sie einen wiederverwendbaren useMqtt-Hook rund um MQTT.js, verbinden Sie React über sicheren WebSocket mit einem Broker und zeigen Sie Live-Sensorwerte an – ohne Re-Render-Stürme, StrictMode-Überraschungen oder verwaiste Verbindungen.

Browser-Dashboard mit Live-Sensorkarten, gebaut mit React MQTT über eine sichere WebSocket-Verbindung
Probieren Sie es auf Ihrem eigenen MQTT-Broker ausHolen Sie sich im Free-Tarif einen privaten, verwalteten MQTT-Broker und starten Sie mit fertigen Codebeispielen.
Kostenlos testen Codebeispiele Keine Kreditkarte erforderlich

Die Kombination aus React und MQTT liefert Dashboards, die sich in dem Moment aktualisieren, in dem ein Gerät publiziert – ohne Polling und ohne eigenen WebSocket-Server. In diesem Tutorial bauen Sie einen wiederverwendbaren useMqtt-Hook rund um MQTT.js, verbinden ihn über sicheren WebSocket mit einem Broker und rendern Live-Sensorkarten, die auch dann schnell bleiben, wenn mehrmals pro Sekunde Nachrichten eintreffen. Alles läuft gegen den kostenlosen öffentlichen MQTT-Broker, sodass Sie in weniger als einer Stunde ein funktionierendes Dashboard haben.

Wie React mit einem MQTT-Broker kommuniziert

Browser können keine rohen TCP-Sockets öffnen, daher kann eine React-App weder Port 1883 noch 8883 nutzen. Stattdessen verbindet sich MQTT.js über WebSocket, und der Broker spricht MQTT innerhalb dieses WebSockets. ConnectMQ-Broker stellen sicheren WebSocket auf Port 8084 mit dem Pfad /mqtt bereit, die URL lautet also:

wss://broker.connectmq.com:8084/mqtt

Verwenden Sie in Apps, die über HTTPS ausgeliefert werden, immer wss://; Browser blockieren unverschlüsseltes ws:// auf sicheren Seiten als Mixed Content. Der Leitfaden MQTT over WebSocket erklärt den Transport ausführlicher.

Projekt-Setup

Erstellen Sie ein Vite-React-Projekt und installieren Sie MQTT.js:

npm create vite@latest react-mqtt-dashboard -- --template react
cd react-mqtt-dashboard
npm install
npm install mqtt
npm run dev

MQTT.js v5 liefert einen Browser-Build mit, den Bundler wie Vite automatisch verwenden, sodass keine Node.js-Polyfills nötig sind.

Den useMqtt-Hook entwerfen

Ein guter Hook hat einen kleinen, vorhersehbaren Vertrag. Unserer nimmt eine URL, Verbindungsoptionen und eine Liste von Topic-Filtern entgegen und liefert den Verbindungsstatus, eine Map mit der letzten Nachricht pro Topic und eine publish-Funktion. Die Designregeln:

Aspekt Entscheidung Grund
Lebensdauer der Verbindung In useEffect erstellt, in dessen Cleanup beendet Keine verwaisten Sockets bei Unmount oder Hot Reload
Doppeltes Mounten in StrictMode Cleanup beendet den ersten Client vollständig In der Entwicklung werden Effects zweimal gemountet
Form des State Letzte Nachricht pro Topic, kein wachsendes Array Speicherverbrauch bleibt konstant, Renders bleiben günstig
Render-Rate Updates alle 250 ms bündeln Schnelle Publisher frieren die UI nicht ein
Identität der Optionen Optionen zu einem Dependency-Key serialisieren Inline-Objekte würden bei jedem Render neu verbinden

Der useMqtt-Hook

Erstellen Sie src/useMqtt.js:

import { useCallback, useEffect, useRef, useState } from "react";
import mqtt from "mqtt";

export function useMqtt(url, options, topics) {
  const [status, setStatus] = useState("connecting");
  const [messages, setMessages] = useState({});
  const clientRef = useRef(null);
  const pendingRef = useRef({});

  // Stabile Keys, damit Inline-Objekte/-Arrays keine Reconnects auslösen
  const optionsKey = JSON.stringify(options);
  const topicsKey = JSON.stringify(topics);

  useEffect(() => {
    const opts = JSON.parse(optionsKey);
    const filters = JSON.parse(topicsKey);
    const client = mqtt.connect(url, {
      protocolVersion: 5,
      reconnectPeriod: 3000,
      connectTimeout: 10000,
      ...opts,
    });
    clientRef.current = client;

    client.on("connect", () => {
      setStatus("connected");
      client.subscribe(filters, { qos: 0 });
    });
    client.on("reconnect", () => setStatus("reconnecting"));
    client.on("offline", () => setStatus("offline"));
    client.on("error", (err) => {
      console.error("MQTT error", err);
      setStatus("error");
    });
    client.on("message", (topic, payload) => {
      let value;
      try {
        value = JSON.parse(payload.toString());
      } catch {
        value = payload.toString();
      }
      pendingRef.current[topic] = { value, receivedAt: Date.now() };
    });

    // Gepufferte Nachrichten höchstens 4-mal pro Sekunde in den React-State übernehmen
    const timer = setInterval(() => {
      const pending = pendingRef.current;
      if (Object.keys(pending).length === 0) return;
      pendingRef.current = {};
      setMessages((prev) => ({ ...prev, ...pending }));
    }, 250);

    return () => {
      clearInterval(timer);
      client.end(true);
      clientRef.current = null;
    };
  }, [url, optionsKey, topicsKey]);

  const publish = useCallback((topic, message, publishOptions = { qos: 0 }) => {
    const client = clientRef.current;
    if (!client || !client.connected) return false;
    const body = typeof message === "string" ? message : JSON.stringify(message);
    client.publish(topic, body, publishOptions);
    return true;
  }, []);

  return { status, messages, publish };
}

Warum er so aufgebaut ist:

  • Nachrichten landen zuerst in einer Ref. Ein setMessages pro Nachricht rendert jedes Mal den Baum neu. Sie in pendingRef zu sammeln und per 250-ms-Timer zu übernehmen, hält die UI flüssig, selbst wenn ein Gerät 20-mal pro Sekunde publiziert.
  • client.end(true) im Cleanup schließt den Socket sofort. In der Entwicklung führt React StrictMode den Effect, das Cleanup und erneut den Effect aus, daher muss der erste Client vollständig geschlossen sein, bevor sich der zweite mit derselben Client ID verbindet. Das erzwungene Beenden verhindert ein kurzes Zeitfenster, in dem sich zwei Sessions mit derselben ID gegenseitig verdrängen.
  • Das Abonnieren im connect-Handler stellt die Subscriptions nach jedem Reconnect wieder her.

Das Dashboard bauen

Ersetzen Sie src/App.jsx durch diese erste Version, die einen Fehler enthält, den wir gleich beheben:

import { useMemo } from "react";
import { useMqtt } from "./useMqtt";

const BROKER_URL = "wss://broker.connectmq.com:8084/mqtt";
const PREFIX = "connectmq/demo/react-58d2";

function SensorCard({ device, reading }) {
  const ageSeconds = Math.round((Date.now() - reading.receivedAt) / 1000);
  const { temperature, humidity } = reading.value ?? {};
  return (
    <div className="card">
      <h3>{device}</h3>
      <p className="big">{temperature ?? "--"} &deg;C</p>
      <p>Humidity: {humidity ?? "--"} %</p>
      <small>{ageSeconds > 30 ? `stale (${ageSeconds}s)` : "live"}</small>
    </div>
  );
}

export default function App() {
  const { status, messages, publish } = useMqtt(
    BROKER_URL,
    { clientId: `react-${Math.random().toString(16).slice(2, 10)}` },
    [`${PREFIX}/+/telemetry`]
  );

  const devices = useMemo(
    () =>
      Object.entries(messages)
        .map(([topic, reading]) => ({ device: topic.split("/").at(-2), reading }))
        .sort((a, b) => a.device.localeCompare(b.device)),
    [messages]
  );

  return (
    <main>
      <header>
        <h1>Live Sensors</h1>
        <span className={`status ${status}`}>{status}</span>
      </header>
      <section className="grid">
        {devices.map(({ device, reading }) => (
          <SensorCard key={device} device={device} reading={reading} />
        ))}
      </section>
      <button onClick={() => publish(`${PREFIX}/fan1/cmd`, { action: "toggle" }, { qos: 1 })}>
        Toggle fan1
      </button>
    </main>
  );
}

Diese Datei enthält einen absichtlichen Fehler: Die clientId wird inline erzeugt, ihr Wert ändert sich also bei jedem Render. Das Serialisieren der Optionen schützt vor neuen Objektidentitäten, aber nicht vor neuen Werten – jeder Render erzeugt daher einen neuen Options-Key und eine brandneue Verbindung. Beheben Sie das, indem Sie die ID nur einmal erzeugen:

import { useMemo, useState } from "react";

// innerhalb von App()
const [clientId] = useState(() => `react-${Math.random().toString(16).slice(2, 10)}`);
const { status, messages, publish } = useMqtt(BROKER_URL, { clientId }, [`${PREFIX}/+/telemetry`]);

Das ist der mit Abstand häufigste Fehler in React-MQTT-Code: Optionen, die sich bei jedem Render ändern und eine Connect-Disconnect-Schleife verursachen, die wie ein Broker-Problem aussieht.

Ergänzen Sie in src/index.css etwas minimales CSS für die Klassen .grid und .card oder verwenden Sie die Komponentenbibliothek Ihrer Wahl.

Testdaten einspeisen

Publizieren Sie Messwerte aus einem Terminal an den öffentlichen MQTT-Broker, der für Tests und Prototypen gedacht ist:

for i in $(seq 1 30); do
  mosquitto_pub -h broker.connectmq.com -p 1883 -i react-pub-58d2 \
    -t connectmq/demo/react-58d2/room1/telemetry \
    -m "{\"temperature\": $((20 + RANDOM % 5)), \"humidity\": $((40 + RANDOM % 10))}"
  sleep 1
done

Die Karten erscheinen, sobald die erste Nachricht des jeweiligen Geräts eintrifft. Klicken Sie auf den Button und beobachten Sie den Befehl mit mosquitto_sub -h broker.connectmq.com -p 1883 -i react-sub-58d2 -v -t "connectmq/demo/react-58d2/+/cmd". Verwenden Sie ein eigenes zufälliges Präfix: Der öffentliche Bereich ist für alle lesbar.

Eine Verbindung über mehrere Komponenten teilen

Der Hook öffnet in dieser Form eine Verbindung pro Komponente, die ihn aufruft. Für eine einzelne Seite ist das ideal, aber ein größeres Dashboard mit Status-Badge im Header, Geräteliste und Detailbereich würde drei Verbindungen öffnen, um sich überschneidende Daten zu empfangen. Verlagern Sie die Verbindung stattdessen in einen Context Provider:

import { createContext, useContext, useState } from "react";
import { useMqtt } from "./useMqtt";

const MqttContext = createContext(null);

export function MqttProvider({ url, topics, children }) {
  const [clientId] = useState(() => `react-${Math.random().toString(16).slice(2, 10)}`);
  const mqttState = useMqtt(url, { clientId }, topics);
  return <MqttContext.Provider value={mqttState}>{children}</MqttContext.Provider>;
}

export function useMqttContext() {
  const value = useContext(MqttContext);
  if (!value) throw new Error("useMqttContext must be used inside MqttProvider");
  return value;
}

Umschließen Sie Ihre App mit <MqttProvider url={BROKER_URL} topics={[...]}> und lesen Sie status, messages und publish an beliebiger Stelle darunter. Jede Komponente rendert neu, wenn sich messages ändert, was bei einem Flush-Intervall von 250 ms akzeptabel ist. Haben Sie Hunderte von Widgets, halten Sie die Nachrichten in einem externen Store wie Zustand oder nutzen Sie useSyncExternalStore mit Selektoren, damit jedes Widget nur neu rendert, wenn sich sein eigenes Topic ändert.

Produktionsaspekte für React-MQTT-Apps

  1. Zugangsdaten im Browser sind öffentlich. Alles in Ihrem JavaScript-Bundle kann von Nutzern gelesen werden. Vergeben Sie für das Dashboard eigene Zugangsdaten, und wenn Nutzer keine Befehle publizieren dürfen, leiten Sie Befehle über eine Backend-API statt über den Browser.
  2. Zeigen Sie den Verbindungsstatus an. Nutzer sollten reconnecting sehen statt stillschweigend veralteter Zahlen. Dafür stellt der Hook status bereit, und jede Karte zeigt das Alter ihrer Daten.
  3. Nutzen Sie Retained Messages für den Anfangszustand. Publizieren Geräte ihren letzten Zustand mit Retain-Flag, füllt sich ein frisch geladenes Dashboard sofort, statt auf die nächste Meldung zu warten.
  4. Achten Sie auf Verbindungslimits. Jeder geöffnete Browser-Tab ist eine MQTT-Verbindung. Berücksichtigen Sie das bei der Dimensionierung Ihres Brokers.
  5. Drosseln Sie Diagramme. Wenn Sie Diagramme ergänzen, fügen Sie Punkte im Flush-Timer hinzu statt pro Nachricht, und begrenzen Sie die Länge der Historie.
  6. Pausieren Sie in Hintergrund-Tabs. Browser drosseln Timer in verborgenen Tabs. Liegt ein Dashboard stundenlang im Hintergrund, beenden Sie die Verbindung bei visibilitychange und verbinden Sie sich neu, sobald der Tab wieder sichtbar ist – Retained Messages füllen die Ansicht dann wieder auf.

Umstieg auf Ihren eigenen Broker

Für ein echtes Dashboard erstellen Sie einen privaten Broker auf ConnectMQ und übergeben die generierten Zugangsdaten an den Hook:

const { status, messages } = useMqtt(
  "wss://YOUR_BROKER_HOST:8084/mqtt",
  { clientId, username: import.meta.env.VITE_MQTT_USERNAME, password: import.meta.env.VITE_MQTT_PASSWORD },
  ["factory1/+/telemetry"]
);

Für ein Fertigungs-Dashboard lohnt es sich, die Topic-Filter an einem Unified Namespace (UNS) auszurichten: Folgen die Topics der ISA-95-Hierarchie, etwa acme/werk-stuttgart/montage/linie-1/zelle-3/telemetry, zeigt ein Filter wie acme/werk-stuttgart/montage/+/+/telemetry alle Zellen eines Bereichs, ohne den Hook zu ändern.

Jeder Tab braucht eine eindeutige Client ID; auf einem Broker, bei dem Client IDs an Zugangsdaten gebunden sind, stellen Sie sicher, dass die Dashboard-Zugangsdaten das zulassen, oder setzen Sie ein kleines Backend zwischen Browser und Broker. Ein Cloud-MQTT-Broker mit Let's-Encrypt-Zertifikat auf dem WebSocket-Port sorgt dafür, dass wss:// in jedem Browser ohne Zertifikatswarnungen funktioniert.

Verwandte Tutorials

Dasselbe Hook-Muster lässt sich auf andere Frameworks übertragen: siehe das Vue-MQTT-Composable und den Angular-MQTT-Service mit RxJS. Für serverseitigen Code beginnen Sie mit MQTT.js in Node.js, und für einen breiteren Vergleich von Bibliotheken mit dem Leitfaden zu MQTT-Client-Bibliotheken.

Starten Sie mit ConnectMQ

Geben Sie Ihrem React-Dashboard in wenigen Minuten einen privaten Broker mit sicherem WebSocket. Registrieren Sie sich unter app.connectmq.com, wählen Sie den kostenlosen Serverless-Plan ohne Kreditkarte, erstellen Sie einen Broker und tragen Sie die Zugangsdaten in useMqtt ein. Der MQTT-Client im Browser des Portals eignet sich perfekt, um Testwerte zu publizieren, während Sie Ihre Komponenten gestalten.

Häufig gestellte Fragen

Kann sich React über Port 1883 mit MQTT verbinden?

Nein. Browser können keine TCP-Sockets öffnen, daher muss React MQTT over WebSocket verwenden. Bei ConnectMQ ist das wss:// auf Port 8084 mit dem Pfad /mqtt.

Warum verbindet sich meine React-App in der Entwicklung zweimal?

React StrictMode mountet Effects in der Entwicklung zweimal, um Cleanup-Fehler aufzudecken. Mit einem korrekten Cleanup, das client.end() aufruft, wird die erste Verbindung sofort geschlossen; Produktions-Builds verbinden sich nur einmal.

Sollte ich den MQTT-Client in einen React Context legen?

Wenn viele Komponenten dieselbe Verbindung brauchen, ja. Erstellen Sie den Client in einem Provider mit derselben Effect-Logik und stellen Sie status, messages und publish über den Context bereit, sodass Sie weiterhin eine Verbindung pro Tab haben.

Gibt es eine offizielle React-MQTT-Bibliothek?

Eine offizielle gibt es nicht. Es existieren kleine Community-Wrapper, aber ein Hook rund um MQTT.js wie in diesem Artikel ist kurz, vollständig unter Ihrer Kontrolle und leicht anzupassen.

Verwandte Artikel

Die besten MQTT-CLI-Tools zum Testen, Skripten und Debuggen

Ein praxisnaher Vergleich von mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, der MQTT.js-CLI, curl und mqttui – mit demselben Test gegen broker.connectmq.com, damit Sie das richtige Tool zum Skripten und Debuggen wählen.

8 Min. Lesezeit

Alle Artikel →

Ihr privater MQTT-Broker ist nur wenige Klicks entfernt

Konto anlegen, Broker-Zugangsdaten erhalten und das erste Gerät verbinden — ohne Server, die installiert, gepatcht oder skaliert werden müssen.