Cómo integrar MQTT en tu app React
¿Buscando cómo integrar MQTT en una app React?
Hay algunos ejemplos online, pero cuando me encontré con este reto, me llevó bastante tiempo conseguir una implementación eficiente y sencilla que cumpliera con mis necesidades.
Te dejo mi sistema, que ahora mismo funciona en producción (eso sí, aquí un poco simplificado).
Tiene dos partes:
- Un contexto: se encarga de conectar automáticamente al servidor MQTT cuando se inicia la app, y de observar los posibles eventos.
- Un custom hook: expone los diferentes métodos para usar MQTT en toda tu app.
Y usaremos el package oficial: MQTT.js.
(Al final del artículo tienes el enlace a una app completa de ejemplo donde puedes ver y probar este mismo código).
Crear el contexto
Lo primero de todo es un contexto. Idealmente envolvería toda tu app.
- Mantiene un estado.
- Observa los posibles eventos y actualiza el estado o ejecuta funciones.
- Conecta al broker MQTT cuando se inicia la app.
- Expone el estado y los métodos necesarios.
Vamos con cada parte, y después tienes el bloque completo del código del MqttProvider
.
Conectar
Esta es básicamente la función para conectar, dentro del Provider del contexto:
import mqtt from "mqtt";
import React from "react";
// mantener el cliente en un estado
const [client, setClient] = React.useState();
// función para conectar
const mqttConnect = React.useCallback(() => {
const mqttClient = mqtt.connect(import.meta.env.VITE_BACKEND_MQTT, {
protocolVersion: 5,
// ... otras opciones que quieras añadir
clientId: getClientId(), // función auxiliar que crea un id único
});
setClient(mqttClient);
}, []);
Al método connect
le pasamos la url del broker (en mi caso algo como “ws://localhost:9001/mqtt”) vía una variable de entorno, y las opciones que queramos.
Guardamos el cliente en el estado del contexto (y luego lo exponemos).
Eventos
En el mismo Provider, dentro de un useEffect
, podemos escuchar diferentes eventos:
if (client) {
// cuando se conecta
client.on("connect", () => {
if (client.connected) setIsConnected(true);
});
// cuando hay error
client.on("error", (err) => {
console.error(err);
client.end();
setIsConnected(false);
});
// cuando se reconecta
client.on("reconnect", () => {
if (client.connected) setIsConnected(true);
});
// cuando se desconecta
client.on("close", () => {
setIsConnected(false);
});
// cuando se recibe un packet de desconexión desde el broker
client.on("disconnect", () => {
setIsConnected(false);
});
// cuando se recibe un mensaje
client.on("message", (topic: string, message: Buffer) => {
try {
// parsear el mensaje (Buffer)
const parsed = JSON.parse(message.toString());
if (parsed) {
// hacer algo con el mensaje
console.log(topic, parsed);
}
} catch (error) {
if (error instanceof Error) throw error;
}
});
}
Básicamente actualizamos el estado isConnected
según el evento que ocurra.
Es un ejemplo básico, hay muchas formas de extenderlo.
- Podrías tener otro estado,
isConnecting
, que también actualizaras (y expusieras). Podrías incluso tener unstatus
con diferentes valores, etc. - Podrías loguear (a consola, archivo, etc.) en cada uno de los eventos.
Gestión de mensajes
En el ejemplo solo se loguean.
En otros ejemplos online tienen un estado dentro del provider:
const [messages, setMessages] = React.useState([]);
Y en cada nuevo mensaje, lo añades a este estado. Siempre podrás ver el mensaje más reciente (el ultimo en el array), y mostrarlos todos en una lista.
Quizás esto sea todo lo que necesitas. Aunque recuerda que si recibes muchos mensajes, tu array se hará gigante. Tendrías que gestionarlo e ir borrando mensajes, etc.
Algo más complejo sería guardar un objecto por cada mensaje, con el timestamp
por ejemplo.
En mi caso, he integrado todo esto con react-query
, y en el callback al recibir un mensaje, lo añado manualmente al caché y no lo mantengo en ningún otro estado. Aunque eso es otra historia.
Autoconectar
En otro useEffect
dentro del mismo Provider, conectamos llamando al método que hemos declarado más arriba.
// auto conectar al inicializar el contexto
React.useEffect(() => {
if (!client && !isConnected) mqttConnect();
}, [client, isConnected]);
Esto se ejecutará una vez al inicializar el Provider (si envuelve tu app entera, se ejecutará cuando se inicia tu app). Después, mientras esté conectado, no debería ejecutarse más veces.
El código completo del Provider
Esta sería el contenido de MqttProvider.tsx
.
Es un bloque largo de código con todas las partes que hemos visto, más el Contexto y algunos detalles que faltaban para unirlo todo. Revísalo.
import mqtt from "mqtt";
import React from "react";
// tipo del contexto
type MqttContextType = {
client: mqtt.MqttClient | null;
isConnected: boolean;
setIsConnected: React.Dispatch<React.SetStateAction<boolean>>;
mqttConnect: () => void;
};
// contenido por defecto del contexto
const defaultContext: MqttContextType = {
client: null,
isConnected: false,
setIsConnected: () => {},
mqttConnect: () => {},
};
// el contexto
const MqttContext = React.createContext<MqttContextType>(defaultContext);
// exportamos un custom hook
export function useMqttContext() {
return React.useContext(MqttContext);
}
// función auxiliar (no la mejor forma de crear ids únicos)
const getClientId = () => `test_client_${Math.random().toString(16).slice(3)}`;
// vamos con el provider
export function MqttProvider({ children }: React.PropsWithChildren) {
const [client, setClient] = React.useState<MqttContextType["client"]>(
defaultContext.client
);
const [isConnected, setIsConnected] = React.useState(
defaultContext.isConnected
);
// función para conectar
const mqttConnect = React.useCallback(() => {
const mqttClient = mqtt.connect(import.meta.env.VITE_BACKEND_MQTT, {
protocolVersion: 5,
// ... otras opciones que quieras añadir
clientId: getClientId(),
});
setClient(mqttClient);
}, []);
// observar eventos
React.useEffect(() => {
if (client) {
// cuando se conecta
client.on("connect", () => {
if (client.connected) setIsConnected(true);
});
// cuando hay error
client.on("error", (err) => {
console.error(err);
client.end();
setIsConnected(false);
});
// cuando se reconecta
client.on("reconnect", () => {
if (client.connected) setIsConnected(true);
});
// cuando se desconecta
client.on("close", () => {
setIsConnected(false);
});
// cuando se recibe un packet de desconexión desde el broker
client.on("disconnect", () => {
setIsConnected(false);
});
// cuando se recibe un mensaje
client.on("message", (topic: string, message: Buffer) => {
try {
// parsear el mensaje (Buffer)
const parsed = JSON.parse(message.toString());
// hacer algo con el mensaje
if (parsed) {
console.log(topic, parsed);
}
} catch (error) {
if (error instanceof Error) throw error;
}
});
}
// cleanup
return () => {
if (client) {
client.endAsync();
setIsConnected(false);
}
};
}, [client]);
// auto conectar al inicializar el contexto
React.useEffect(() => {
if (!client && !isConnected) mqttConnect();
}, [client, isConnected, mqttConnect]);
const contextValue = React.useMemo(
() => ({
client,
isConnected,
setIsConnected,
mqttConnect,
}),
[client, isConnected, mqttConnect]
);
return (
<MqttContext.Provider value={contextValue}>{children}</MqttContext.Provider>
);
}
Crear hook con métodos
Expone los diferentes métodos para usar MQTT en toda la app:
- Desconectar el cliente.
- Suscribirte a topics.
- Desuscribirte.
- Y publicar mensajes.
Igual que antes, vamos con cada función que se crea dentro del hook useMqtt
, y después tienes el hook completo:
Desconectar
// desconectar
async function mqttDisconnect() {
if (isConnected && client) {
try {
await client.endAsync();
setIsConnected(false);
} catch (err) {
console.error(err);
}
}
}
Suscribir
Para recibir un mensaje en un topic, necesitas previamente haberte suscrito.
// suscribir. el topic se pasa como parámetro
async function mqttSubscribe(topic: string) {
if (isConnected && client) {
try {
await client.subscribeAsync(topic, {
qos: 1,
rap: false,
rh: 0,
});
} catch (err) {
console.error(err);
}
}
}
Desuscribir
// desuscribir
async function mqttUnSubscribe(topic: string) {
if (isConnected && client) {
try {
await client.unsubscribeAsync(topic);
} catch (err) {
console.error(err);
}
}
}
Publicar
// publicar mensaje
async function mqttPublish(topic: string, message: string | number | boolean) {
if (client && isConnected) {
try {
await client.publishAsync(topic, JSON.stringify(message), {
qos: 1,
});
} catch (err) {
console.error(err);
}
}
}
Recuerda que puedes publicar un mensaje en un topic sin estar suscrito a este. Tu mensaje se enviará, pero no lo verás en tu app (no llegará el evento “message”) si no te has suscrito previamente.
El código completo del hook
En este ejemplo, básicamente son funciones que envuelven los métodos que ya expone MQTT.js. Quizás te parezca que no tiene mucho sentido, pero en cada una de estas funciones podrías tener lógica adicional relativa a tu app:
- Validar que el topic esté en una lista de topics permitidos.
- Validar la estructura/caracteres/etc. del mensaje a publicar.
- Loguear en cada función, a consola/archivo/endpoint/etc.
- Devolver una respuesta que después usarías para mostrar una confirmación al usuario.
- etc.
Esto sería useMqtt.ts
:
import { useMqttContext } from "./MqttProvider";
export function useMqtt() {
const { client, isConnected, setIsConnected, mqttConnect } = useMqttContext();
// desconectar
async function mqttDisconnect() {
if (isConnected && client) {
try {
await client.endAsync();
setIsConnected(false);
} catch (err) {
console.error(err);
}
}
}
// suscribir. el topic se pasa como parámetro
async function mqttSubscribe(topic: string) {
if (isConnected && client) {
try {
await client.subscribeAsync(topic, {
qos: 1,
rap: false,
rh: 0,
});
} catch (err) {
console.error(err);
}
}
}
// desuscribir
async function mqttUnSubscribe(topic: string) {
if (isConnected && client) {
try {
await client.unsubscribeAsync(topic);
} catch (err) {
console.error(err);
}
}
}
// publicar mensaje
async function mqttPublish(
topic: string,
message: string | number | boolean
) {
if (client && isConnected) {
try {
await client.publishAsync(topic, JSON.stringify(message), {
qos: 1,
});
} catch (err) {
console.error(err);
}
}
}
return {
client,
isConnected,
mqttConnect,
mqttDisconnect,
mqttPublish,
mqttSubscribe,
mqttUnSubscribe,
};
}
Este hook también re-expone client
e isConnected
, los estados del Provider, y mqttConnect
, para que puedan usarse en el resto de la app desde este hook.
Cómo usar todo esto
Con estos dos elementos, ya tienes todo lo que necesitas para integrar MQTT en tu app React.
Vamos a ver cómo encaja.
El contexto, con MqttProvider
En el archivo de entrada de tu app, meteríamos todos los componentes dentro de MqttProvider:
import { MqttProvider } from "./MqttContext.tsx";
createRoot(document.getElementById("root")!).render(
<StrictMode>
<MqttProvider>
<App />
</MqttProvider>
</StrictMode>
);
Los métodos, con useMqtt()
Cuando queramos conectar, publicar, etc., por ejemplo al clicar un botón, podemos importar las funciones desde el hook useMqtt()
y usarlas.
import { useMqtt } from "./useMqtt";
const MqttConnectButton = () => {
const { mqttConnect } = useMqtt();
return (
<button
onClick={() => {
mqttConnect();
}}
>
Connect
</button>
);
};
//...
App de ejemplo
Para que lo veas más claro, he creado una app muy sencilla que utiliza el mismo código que tienes en estos ejemplos.
Puedes clonarla y problarla.
Necesitarás un broker. Yo uso mosquitto para hacer pruebas, iniciado como un servicio que siempre está activo por detrás.
Puedes abrir varias instancias de la misma app e intercambiar mensajes entre ellas, siempre que te suscribas al mismo topic. Verás los mensajes en la consola del navegador.
También puedes probarla con una app de testing MQTT, como MQTT Explorer, MQTTX, o MQTT Multimeter.
No hay sección de comentarios, pero me encantaría escuchar tu opinión: escríbeme en Twitter y cuéntame!