Associer React et MQTT vous donne des tableaux de bord qui se mettent à jour à l'instant même où un appareil publie, sans polling ni serveur WebSocket sur mesure. Dans ce tutoriel, vous allez construire un hook useMqtt réutilisable autour de MQTT.js, le connecter à un broker en WebSocket sécurisé et afficher des cartes de capteurs en direct qui restent fluides même quand les messages arrivent plusieurs fois par seconde. Tout fonctionne avec le serveur MQTT public gratuit : vous pouvez obtenir un tableau de bord opérationnel en moins d'une heure.
Comment React communique avec un broker MQTT
Les navigateurs ne peuvent pas ouvrir de sockets TCP bruts : une application React ne peut donc pas utiliser les ports 1883 ou 8883. MQTT.js se connecte plutôt en WebSocket, et le broker parle MQTT à l'intérieur de ce WebSocket. Les brokers ConnectMQ exposent le WebSocket sécurisé sur le port 8084 avec le chemin /mqtt, l'URL est donc :
wss://broker.connectmq.com:8084/mqtt
Utilisez toujours wss:// dans les applications servies en HTTPS ; les navigateurs bloquent ws:// en clair depuis les pages sécurisées en tant que contenu mixte. Le guide MQTT over WebSocket explique ce transport plus en détail.
Mise en place du projet
Créez un projet React avec Vite et installez 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 fournit une version navigateur que les bundlers comme Vite détectent automatiquement : aucun polyfill Node.js n'est nécessaire.
Concevoir le hook useMqtt
Un bon hook a un contrat réduit et prévisible. Le nôtre prend une URL, des options de connexion et une liste de filtres de topics, et renvoie l'état de la connexion, une table du dernier message par topic et une fonction publish. Les règles de conception :
| Problématique | Décision | Raison |
|---|---|---|
| Durée de vie de la connexion | Créée dans useEffect, fermée dans son cleanup |
Aucune fuite de socket au démontage ou au hot reload |
| Double montage de StrictMode | Le cleanup ferme complètement le premier client | En développement, les effets sont montés deux fois |
| Forme de l'état | Dernier message par topic, pas un tableau qui grossit | La mémoire reste stable, les rendus restent légers |
| Fréquence de rendu | Mises à jour groupées toutes les 250 ms | Les publishers rapides ne figent pas l'interface |
| Identité des options | Options sérialisées en clé de dépendance | Des objets inline provoqueraient une reconnexion à chaque rendu |
Le hook useMqtt
Créez 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({});
// Clés stables pour que les objets/tableaux inline ne déclenchent pas de reconnexion
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() };
});
// Vider les messages en tampon dans le state React au maximum 4 fois par seconde
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 };
}
Pourquoi il est construit ainsi :
- Les messages arrivent d'abord dans une ref. Appeler
setMessagespour chaque message re-rend l'arbre à chaque fois. Les collecter danspendingRefet les vider sur un timer de 250 ms garde l'interface fluide, même si un appareil publie 20 fois par seconde. client.end(true)dans le cleanup ferme immédiatement le socket. En développement, le StrictMode de React exécute l'effet, le cleanup, puis de nouveau l'effet : le premier client doit donc être complètement fermé avant que le second ne se connecte avec le même client ID. Forcer la fermeture évite une courte fenêtre pendant laquelle deux sessions avec le même ID se combattent.- S'abonner dans le handler
connectrétablit les abonnements après chaque reconnexion.
Construire le tableau de bord
Remplacez src/App.jsx par cette première version, qui contient un bug que nous corrigerons dans un instant :
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 ?? "--"} °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>
);
}
Ce fichier contient un bug volontaire : le clientId est généré inline, sa valeur change donc à chaque rendu. Sérialiser les options protège contre les nouvelles identités d'objet, mais pas contre les nouvelles valeurs : chaque rendu produit une nouvelle clé d'options et une toute nouvelle connexion. Corrigez-le en générant l'ID une seule fois :
import { useMemo, useState } from "react";
// dans App()
const [clientId] = useState(() => `react-${Math.random().toString(16).slice(2, 10)}`);
const { status, messages, publish } = useMqtt(BROKER_URL, { clientId }, [`${PREFIX}/+/telemetry`]);
C'est de loin le bug le plus courant dans le code React MQTT : des options qui changent à chaque rendu, provoquant une boucle de connexions et de déconnexions qui ressemble à un problème de broker.
Ajoutez un peu de CSS minimal dans src/index.css pour les classes .grid et .card, ou utilisez la bibliothèque de composants de votre choix.
L'alimenter avec des données de test
Publiez des mesures depuis un terminal vers le serveur MQTT public gratuit :
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
Les cartes apparaissent dès l'arrivée du premier message de chaque appareil. Cliquez sur le bouton et observez la commande avec mosquitto_sub -h broker.connectmq.com -p 1883 -i react-sub-58d2 -v -t "connectmq/demo/react-58d2/+/cmd". Utilisez votre propre préfixe aléatoire : l'espace public est lisible par tout le monde.
Partager une seule connexion entre composants
Tel qu'il est écrit, le hook ouvre une connexion par composant qui l'appelle. C'est parfait pour une page unique, mais un tableau de bord plus vaste avec un badge de statut dans l'en-tête, une liste d'appareils et un panneau de détail ouvrirait trois connexions pour recevoir des données qui se recoupent. Remontez plutôt la connexion dans un 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;
}
Enveloppez votre application dans <MqttProvider url={BROKER_URL} topics={[...]}> et lisez status, messages et publish n'importe où en dessous. Chaque composant se re-rend quand messages change, ce qui reste acceptable avec un intervalle de vidage de 250 ms. Si vous avez des centaines de widgets, conservez les messages dans un store externe comme Zustand ou utilisez useSyncExternalStore avec des sélecteurs, afin que chaque widget ne se re-rende que lorsque son propre topic change.
Points d'attention en production pour les applications React MQTT
- Les identifiants dans le navigateur sont publics. Tout ce qui se trouve dans votre bundle JavaScript peut être lu par les utilisateurs. Délivrez un identifiant dédié au tableau de bord et, si les utilisateurs ne doivent pas publier de commandes, faites transiter les commandes par une API backend plutôt que par le navigateur.
- Affichez l'état de la connexion. Les utilisateurs doivent voir
reconnectingplutôt que des chiffres silencieusement périmés. Le hook exposestatusà cet effet, et chaque carte indique l'âge de ses données. - Utilisez les retained messages pour l'état initial. Si les appareils publient leur dernier état avec le flag retain, un tableau de bord fraîchement chargé se remplit instantanément au lieu d'attendre le prochain rapport.
- Surveillez les limites de connexions. Chaque onglet de navigateur ouvert représente une connexion MQTT. Tenez-en compte lors du dimensionnement de votre broker.
- Limitez la fréquence des graphiques. Si vous ajoutez des graphiques, ajoutez les points au rythme du timer de vidage plutôt qu'à chaque message, et plafonnez la longueur de l'historique.
- Mettez en pause dans les onglets en arrière-plan. Les navigateurs ralentissent les timers des onglets masqués. Si un tableau de bord reste en arrière-plan pendant des heures, envisagez de fermer la connexion sur
visibilitychangeet de vous reconnecter quand l'onglet redevient visible, en laissant les retained messages remplir à nouveau la vue.
Passer à votre propre broker
Pour un véritable tableau de bord, créez un broker privé sur ConnectMQ, un broker MQTT managé, et passez les identifiants générés au 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"]
);
Chaque onglet a besoin d'un client ID unique ; sur un broker où les client ID sont liés aux identifiants, vérifiez que l'identifiant du tableau de bord le permet, ou placez un petit backend entre le navigateur et le broker. Avec un broker MQTT cloud doté d'un certificat Let's Encrypt sur le port WebSocket, wss:// fonctionne dans tous les navigateurs sans avertissement de certificat.
Tutoriels associés
Le même pattern de hook se transpose à d'autres frameworks : voir le composable Vue MQTT et le service Angular MQTT avec RxJS. Pour le code côté serveur, commencez par MQTT.js avec Node.js, et pour une comparaison plus large des bibliothèques, consultez le guide des bibliothèques clientes MQTT.
Démarrer avec ConnectMQ
Offrez à votre tableau de bord React un broker privé avec WebSocket sécurisé en quelques minutes. Inscrivez-vous sur app.connectmq.com, choisissez l'offre Serverless gratuite sans carte bancaire, créez un broker et branchez les identifiants dans useMqtt. Le client MQTT intégré au navigateur du portail est idéal pour publier des valeurs de test pendant que vous concevez vos composants.
Questions fréquentes
React peut-il se connecter à MQTT sur le port 1883 ?
Non. Les navigateurs ne peuvent pas ouvrir de sockets TCP : React doit donc utiliser MQTT over WebSocket. Sur ConnectMQ, il s'agit de wss:// sur le port 8084 avec le chemin /mqtt.
Pourquoi mon application React se connecte-t-elle deux fois en développement ?
Le StrictMode de React monte les effets deux fois en développement pour révéler les bugs de cleanup. Avec un cleanup correct qui appelle client.end(), la première connexion se ferme immédiatement ; les builds de production ne se connectent qu'une fois.
Faut-il placer le client MQTT dans un context React ?
Si de nombreux composants ont besoin de la même connexion, oui. Créez le client dans un provider avec la même logique d'effet, et exposez status, messages et publish via le context afin de conserver une seule connexion par onglet.
Existe-t-il une bibliothèque React MQTT officielle ?
Il n'en existe pas d'officielle. Quelques petites surcouches communautaires existent, mais un hook autour de MQTT.js comme celui de cet article est court, entièrement sous votre contrôle et facile à adapter.



