Combinar React y MQTT te da dashboards que se actualizan en el instante en que un dispositivo publica, sin polling y sin un servidor WebSocket propio. En este tutorial crearás un hook useMqtt reutilizable sobre MQTT.js, lo conectarás a un broker mediante WebSocket seguro y mostrarás tarjetas de sensores en vivo que siguen siendo rápidas aunque los mensajes lleguen varias veces por segundo. Todo funciona contra el servidor MQTT público gratuito, así que puedes tener un dashboard operativo en menos de una hora.
Cómo habla React con un broker MQTT
Los navegadores no pueden abrir sockets TCP directos, así que una app React no puede usar el puerto 1883 ni el 8883. En su lugar, MQTT.js se conecta por WebSocket y el broker habla MQTT dentro de ese WebSocket. Los brokers de ConnectMQ exponen WebSocket seguro en el puerto 8084 con la ruta /mqtt, así que la URL es:
wss://broker.connectmq.com:8084/mqtt
Usa siempre wss:// en apps servidas por HTTPS; los navegadores bloquean ws:// sin cifrar desde páginas seguras por ser contenido mixto. La guía de MQTT sobre WebSocket explica el transporte con más detalle.
Configuración del proyecto
Crea un proyecto React con Vite e instala 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 incluye una versión para navegador que bundlers como Vite detectan automáticamente, así que no necesitas polyfills de Node.js.
Diseño del hook useMqtt
Un buen hook tiene un contrato pequeño y predecible. El nuestro recibe una URL, opciones de conexión y una lista de filtros de topic, y devuelve el estado de la conexión, un mapa con el último mensaje de cada topic y una función publish. Reglas de diseño:
| Aspecto | Decisión | Motivo |
|---|---|---|
| Vida de la conexión | Se crea en useEffect y se cierra en su limpieza |
Sin sockets huérfanos al desmontar o con hot reload |
| Doble montaje de StrictMode | La limpieza cierra por completo el primer cliente | En desarrollo los efectos se montan dos veces |
| Forma del estado | Último mensaje por topic, no un array que crece | La memoria se mantiene estable y los renders son baratos |
| Frecuencia de render | Actualizaciones agrupadas cada 250 ms | Los publicadores rápidos no congelan la UI |
| Identidad de las opciones | Serializar las opciones en una clave de dependencia | Los objetos inline reconectarían en cada render |
El hook useMqtt
Crea 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({});
// Claves estables para que objetos/arrays inline no provoquen reconexiones
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() };
});
// Vuelca los mensajes almacenados al estado de React como máximo 4 veces por segundo
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 };
}
Por qué está construido así:
- Los mensajes van primero a un ref. Llamar a
setMessagescon cada mensaje vuelve a renderizar el árbol cada vez. Acumularlos enpendingRefy volcarlos con un temporizador de 250 ms mantiene la UI fluida aunque un dispositivo publique 20 veces por segundo. client.end(true)en la limpieza cierra el socket de inmediato. En desarrollo, React StrictMode ejecuta el efecto, la limpieza y de nuevo el efecto, así que el primer cliente debe estar totalmente cerrado antes de que el segundo se conecte con el mismo client ID. Forzar el cierre evita un breve intervalo en el que dos sesiones con un mismo ID se pelean entre sí.- Suscribirse en el handler
connectrestaura las suscripciones tras cada reconexión.
Construir el dashboard
Sustituye src/App.jsx por esta primera versión, que contiene un bug que corregiremos enseguida:
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>
);
}
En este archivo hay un bug intencionado: el clientId se genera inline, así que su valor cambia en cada render. Serializar las opciones protege frente a identidades de objeto nuevas, pero no frente a valores nuevos, de modo que cada render produce una clave de opciones nueva y una conexión completamente nueva. Corrígelo generando el ID una sola vez:
import { useMemo, useState } from "react";
// dentro de App()
const [clientId] = useState(() => `react-${Math.random().toString(16).slice(2, 10)}`);
const { status, messages, publish } = useMqtt(BROKER_URL, { clientId }, [`${PREFIX}/+/telemetry`]);
Este es el bug más común en el código de React con MQTT: opciones que cambian en cada render y provocan un bucle de conexión y desconexión que parece un problema del broker.
Añade algo de CSS mínimo en src/index.css para las clases .grid y .card, o usa la librería de componentes que prefieras.
Aliméntalo con datos de prueba
Publica lecturas desde un terminal en el servidor público, pensado para pruebas y no para datos de producción:
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
Las tarjetas aparecen en cuanto llega el primer mensaje de cada dispositivo. Pulsa el botón y observa el comando con mosquitto_sub -h broker.connectmq.com -p 1883 -i react-sub-58d2 -v -t "connectmq/demo/react-58d2/+/cmd". Usa tu propio prefijo aleatorio: el área pública la puede leer cualquiera.
Compartir una conexión entre componentes
Tal como está escrito, el hook abre una conexión por cada componente que lo llama. Es perfecto para una sola página, pero un dashboard más grande con un indicador de estado en la cabecera, una lista de dispositivos y un panel de detalle abriría tres conexiones para recibir datos que se solapan. En su lugar, sube la conexión a 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;
}
Envuelve tu app en <MqttProvider url={BROKER_URL} topics={[...]}> y lee status, messages y publish en cualquier punto por debajo. Todos los componentes se vuelven a renderizar cuando cambia messages, algo aceptable con un intervalo de volcado de 250 ms. Si tienes cientos de widgets, guarda los mensajes en un store externo como Zustand o usa useSyncExternalStore con selectores, para que cada widget solo se renderice cuando cambia su propio topic.
Consideraciones de producción para apps React con MQTT
- Las credenciales en el navegador son públicas. Cualquier cosa de tu bundle JavaScript la pueden leer los usuarios. Emite una credencial dedicada para el dashboard y, si los usuarios no deben publicar comandos, envía los comandos a través de una API de backend en lugar de desde el navegador.
- Muestra el estado de la conexión. Los usuarios deberían ver
reconnectingen lugar de números obsoletos en silencio. El hook exponestatuspara ello y cada tarjeta muestra la antigüedad de sus datos. - Usa retained messages para el estado inicial. Si los dispositivos publican su último estado con el flag retain, un dashboard recién cargado se llena al instante en lugar de esperar al siguiente informe.
- Ten en cuenta los límites de conexiones. Cada pestaña abierta del navegador es una conexión MQTT. Tenlo previsto al dimensionar tu broker.
- Limita la frecuencia de los gráficos. Si añades gráficos, agrega puntos en el temporizador de volcado en lugar de por mensaje y limita la longitud del histórico.
- Pausa en pestañas en segundo plano. Los navegadores ralentizan los temporizadores en las pestañas ocultas. Si un dashboard pasa horas en segundo plano, plantéate cerrar la conexión en
visibilitychangey reconectar cuando la pestaña vuelva a estar visible, dejando que los retained messages rellenen la vista.
Pasar a tu propio broker
Para un dashboard real, crea un broker privado en ConnectMQ, un broker MQTT gestionado, y pasa las credenciales generadas al 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"]
);
Cada pestaña necesita un client ID único; en un broker donde los client IDs están ligados a las credenciales, asegúrate de que la credencial del dashboard lo permita o pon un pequeño backend entre el navegador y el broker. Un broker MQTT en la nube con un certificado de Let's Encrypt en el puerto WebSocket hace que wss:// funcione en cualquier navegador sin avisos de certificado.
Tutoriales relacionados
El mismo patrón de hook se traslada a otros frameworks: consulta el composable de Vue para MQTT y el servicio MQTT de Angular con RxJS. Para código de servidor, empieza por MQTT.js en Node.js, y para una comparación más amplia de librerías, la guía de librerías cliente MQTT. Y si las tarjetas se te quedan cortas, los mismos mensajes pueden animar un gemelo digital 3D de la planta directamente en el navegador.
Empieza con ConnectMQ
Dale a tu dashboard React un broker privado con WebSocket seguro en pocos minutos. Regístrate en app.connectmq.com, elige el plan Serverless gratuito sin tarjeta de crédito, crea un broker y conecta las credenciales a useMqtt. El cliente MQTT del portal en el navegador es perfecto para publicar valores de prueba mientras diseñas tus componentes.
Preguntas frecuentes
¿Puede React conectarse a MQTT en el puerto 1883?
No. Los navegadores no pueden abrir sockets TCP, así que React debe usar MQTT sobre WebSocket. En ConnectMQ eso es wss:// en el puerto 8084 con la ruta /mqtt.
¿Por qué mi app React se conecta dos veces en desarrollo?
React StrictMode monta los efectos dos veces en desarrollo para sacar a la luz errores de limpieza. Con una limpieza adecuada que llame a client.end(), la primera conexión se cierra de inmediato; las builds de producción se conectan una sola vez.
¿Debo poner el cliente MQTT en un context de React?
Si muchos componentes necesitan la misma conexión, sí. Crea el cliente en un provider con la misma lógica de efecto y expón status, messages y publish mediante el context, para seguir teniendo una conexión por pestaña.
¿Existe una librería oficial de React para MQTT?
No hay ninguna oficial. Existen pequeños envoltorios de la comunidad, pero un hook sobre MQTT.js como el de este artículo es corto, está totalmente bajo tu control y es fácil de adaptar.



