Tutoriales de IoT

React Native MQTT: apps IoT móviles en tiempo real sobre WebSocket

Crea una app React Native MQTT con MQTT.js sobre WebSocket seguro: un hook reutilizable, datos de dispositivos en vivo, comandos y una gestión correcta de las reconexiones cuando la app pasa entre segundo y primer plano.

App React Native MQTT en Android y iPhone recibiendo datos IoT en vivo desde un broker en la nube por WebSocket seguro
Pruébalo en tu propio broker MQTTConsigue un broker MQTT privado y gestionado con el plan Free y empieza con ejemplos de código listos para usar.
Pruébalo gratis Ejemplos de código Sin tarjeta

React Native MQTT da a tu app móvil datos de dispositivos en vivo sin tener que sondear una API. En este tutorial conectarás una app React Native (o Expo) a un broker en la nube con MQTT.js sobre WebSocket seguro, la envolverás en un hook reutilizable, enviarás comandos a un dispositivo y gestionarás las transiciones entre segundo y primer plano que hacen fracasar la mayoría de los primeros intentos.

Por qué WebSocket para React Native MQTT

El runtime de JavaScript de React Native no tiene sockets TCP sin procesar. Puedes añadirlos con un módulo nativo, pero eso implica pasos de compilación nativa, dev clients de Expo y errores específicos de cada plataforma. React Native sí incluye un WebSocket conforme al estándar, y MQTT.js ya sabe ejecutar MQTT sobre él. El resultado es un cliente en JavaScript puro que funciona en Expo Go, en Android y en iOS sin código nativo.

ConnectMQ expone WebSocket seguro en el puerto 8084 con la ruta /mqtt, con un certificado de confianza pública, así que la URL que necesitas es:

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

El puerto 8084 también atraviesa la mayoría de las redes corporativas y de operadores móviles porque se parece a HTTPS. Para el contexto completo, consulta MQTT sobre WebSocket.

Configuración del proyecto

Crea un proyecto e instala MQTT.js junto con un polyfill de URL (la implementación integrada de URL en React Native está incompleta, y MQTT.js analiza la URL del broker):

npx create-expo-app@latest mqtt-demo
cd mqtt-demo
npx expo install react-native-url-polyfill
npm install mqtt

MQTT.js 5 declara una entrada react-native en los exports de su paquete que apunta a su bundle autónomo para navegador, así que las versiones recientes de Metro resuelven import mqtt from "mqtt" correctamente. Si tu bundler toma en su lugar la build de Node (verás errores sobre net, tls o fs), importa el bundle de forma explícita con import mqtt from "mqtt/dist/mqtt.esm".

Al principio de todo de tu archivo de entrada (app/_layout.tsx en Expo Router, o index.js en una app bare), carga el polyfill una vez:

import "react-native-url-polyfill/auto";

Construir un hook de React Native MQTT

Un hook personalizado mantiene la lógica de conexión fuera de los componentes y facilita compartir un único cliente entre pantallas. Esta versión se conecta al montarse, se suscribe, expone los mensajes y el estado de la conexión, y reacciona a los cambios de AppState.

hooks/useMqtt.js:

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

const BROKER_URL = "wss://broker.connectmq.com:8084/mqtt";

export function useMqtt({ topics, username, password, clientId }) {
  const clientRef = useRef(null);
  const [status, setStatus] = useState("idle");
  const [messages, setMessages] = useState({});
  const idRef = useRef(clientId || `rn-${Math.random().toString(16).slice(2, 10)}`);

  const connect = useCallback(() => {
    if (clientRef.current) return;

    const client = mqtt.connect(BROKER_URL, {
      clientId: idRef.current,
      username,
      password,
      protocolVersion: 5,
      clean: true,
      keepalive: 30,
      reconnectPeriod: 2000,
      connectTimeout: 10000,
    });
    clientRef.current = client;
    setStatus("connecting");

    client.on("connect", () => {
      setStatus("connected");
      client.subscribe(topics, { qos: 1 }, (err) => {
        if (err) console.warn("subscribe failed", err.message);
      });
    });
    client.on("reconnect", () => setStatus("reconnecting"));
    client.on("offline", () => setStatus("offline"));
    client.on("error", (err) => console.warn("mqtt error", err.message));
    client.on("message", (topic, payload) => {
      setMessages((prev) => ({ ...prev, [topic]: payload.toString() }));
    });
  }, [topics, username, password]);

  const disconnect = useCallback(() => {
    const client = clientRef.current;
    clientRef.current = null;
    if (client) client.end(true);
    setStatus("idle");
  }, []);

  const publish = useCallback((topic, message) => {
    clientRef.current?.publish(topic, message, { qos: 1 });
  }, []);

  useEffect(() => {
    connect();
    const sub = AppState.addEventListener("change", (state) => {
      if (state === "active") connect();
      else if (state === "background") disconnect();
    });
    return () => {
      sub.remove();
      disconnect();
    };
  }, [connect, disconnect]);

  return { status, messages, publish, clientId: idRef.current };
}

Fíjate en client.end(true): el true fuerza el cierre inmediato del socket en lugar de esperar a los mensajes en curso, algo importante porque el sistema operativo solo te concede unos segundos antes de suspender la app.

Gestionar las reconexiones entre segundo y primer plano

Aquí es donde fallan la mayoría de las apps React Native MQTT, así que merece una sección propia. Esto es lo que ocurre realmente en un teléfono:

  1. El usuario cambia de app. En cuestión de segundos iOS suspende tu hilo de JavaScript; Android puede mantenerlo vivo brevemente y después restringe el acceso a la red.
  2. Desde el punto de vista de la app, el WebSocket sigue "abierto", pero no se envían pings de keep-alive. El broker cierra la sesión tras aproximadamente 1,5 veces el intervalo de keep-alive.
  3. El usuario vuelve. MQTT.js acaba notando el socket muerto, pero solo tras su propio timeout, así que la interfaz muestra valores obsoletos y "conectado" durante varios segundos.

El hook lo evita cerrando deliberadamente en background y abriendo una conexión nueva en active. Algunas reglas más ayudan:

  • Suscríbete dentro del manejador de connect, no una sola vez tras crear el cliente. MQTT.js se vuelve a suscribir por defecto en sus propias reconexiones, pero un cliente totalmente nuevo tras volver al primer plano necesita una suscripción nueva, y hacerlo en el manejador cubre ambos casos.
  • Usa mensajes retenidos para el estado. Si los dispositivos publican su último estado con el flag retain, una app recién conectada recibe el valor actual de inmediato en lugar de esperar a la siguiente actualización. Consulta mensajes retenidos en MQTT.
  • No mantengas conexiones en segundo plano para alertas. Usa las notificaciones push de la plataforma para todo lo que deba llegar a una app cerrada; MQTT es para datos en vivo mientras la app está abierta.
  • Ignora inactive en iOS. Se dispara con el centro de notificaciones o una llamada entrante; cerrar la conexión ahí provoca reconexiones innecesarias.
AppState Qué hace el hook Por qué
active connect() si no existe ningún cliente Socket y suscripciones nuevos
inactive (solo iOS) nada Transitorio, p. ej. al desplegar el centro de control
background disconnect() El sistema operativo suspenderá el socket de todos modos

Usar el hook en una pantalla

import { View, Text, Switch, StyleSheet } from "react-native";
import { useMemo, useState } from "react";
import { useMqtt } from "../hooks/useMqtt";

const PREFIX = "connectmq/demo/rn-7f3a91";   // elige tu propio prefijo aleatorio

export default function DeviceScreen() {
  const topics = useMemo(() => [`${PREFIX}/pump/status`, `${PREFIX}/tank/level`], []);
  const { status, messages, publish } = useMqtt({ topics });
  const [pumpOn, setPumpOn] = useState(false);

  const level = messages[`${PREFIX}/tank/level`] ?? "--";
  const pumpStatus = messages[`${PREFIX}/pump/status`] ?? "unknown";

  return (
    <View style={styles.container}>
      <Text style={styles.status}>Broker: {status}</Text>
      <Text style={styles.value}>{level} %</Text>
      <Text>Tank level</Text>
      <Text style={styles.row}>Pump reports: {pumpStatus}</Text>
      <View style={styles.row}>
        <Text>Pump command</Text>
        <Switch
          value={pumpOn}
          disabled={status !== "connected"}
          onValueChange={(v) => {
            setPumpOn(v);
            publish(`${PREFIX}/pump/set`, v ? "ON" : "OFF");
          }}
        />
      </View>
    </View>
  );
}

const styles = StyleSheet.create({
  container: { flex: 1, padding: 24, justifyContent: "center", gap: 8 },
  status: { color: "#666" },
  value: { fontSize: 56, fontWeight: "600" },
  row: { marginTop: 16, flexDirection: "row", justifyContent: "space-between", alignItems: "center" },
});

El useMemo sobre el array de topics es importante: sin él, un array nuevo en cada render cambia el callback connect y provoca bucles de reconexión.

Compartir una conexión entre pantallas

Tal como está escrito, el hook abre una conexión por cada componente que lo usa. Para una app con varias pantallas, sube el cliente a un provider de contexto de React en la raíz: el provider llama a useMqtt una vez con la unión de todos los topics y expone messages y publish a través del contexto, y cada pantalla lee solo los topics que necesita. Una conexión por app te mantiene holgadamente dentro de los límites de conexiones, ya que el plan gratuito Serverless permite 25 conexiones simultáneas en total, y evita que el broker vea varias sesiones compitiendo desde un mismo teléfono. Si las pantallas necesitan suscripciones distintas con el tiempo, añade funciones subscribe y unsubscribe al contexto en lugar de reconectar.

Pruebas contra el broker público

El servidor MQTT público gratuito en broker.connectmq.com acepta clientes anónimos, así que puedes probar sin cuenta. Está pensado para pruebas y prototipos, no para datos de producción: mantén el prefijo aleatorio, porque el área pública es legible por todo el mundo. Simula el depósito y la bomba desde una terminal:

PREFIX="connectmq/demo/rn-7f3a91"

# Retenido para que la app lo vea justo después de conectarse
mosquitto_pub -h broker.connectmq.com -p 1883 -i rn-sim-tank -r \
  -t "$PREFIX/tank/level" -m "64"

mosquitto_pub -h broker.connectmq.com -p 1883 -i rn-sim-pump -r \
  -t "$PREFIX/pump/status" -m "OFF"

mosquitto_sub -h broker.connectmq.com -p 1883 -i rn-sim-cmd -t "$PREFIX/pump/set" -v

Pasa la app a segundo plano durante un minuto, tráela de vuelta y comprueba que el estado pasa de connecting a connected y que los valores retenidos reaparecen al instante. El broker público permite hasta 10 mensajes por segundo por cliente, más que suficiente para una interfaz.

Producción: broker privado y credenciales

Para una app publicada, pasa a un broker privado en un broker MQTT gestionado:

  1. Crea un broker en el portal de ConnectMQ y copia el usuario, la contraseña y el client ID generados.
  2. Pásalos al hook: useMqtt({ topics, username: "YOUR_USERNAME", password: "YOUR_PASSWORD", clientId: "YOUR_CLIENT_ID" }).
  3. Guarda los secretos con expo-secure-store o el llavero de la plataforma, nunca en el bundle de JavaScript, que cualquiera puede desempaquetar.

ConnectMQ vincula los client ID a las credenciales que emite, así que dos teléfonos no pueden compartir una misma identidad al mismo tiempo. Para una app multiusuario, haz que tu backend entregue a cada usuario sus propias credenciales tras iniciar sesión. El plan gratuito Serverless permite hasta 25 conexiones simultáneas, suficiente para un piloto en equipo; los planes Serverless de pago llegan a 1.000.

Solución de problemas

Síntoma Causa Solución
Cannot read property 'protocol' of undefined o errores de URL Falta el polyfill de URL Importa primero react-native-url-polyfill/auto
Errores que mencionan net o tls Metro resolvió la build de Node Importa mqtt/dist/mqtt.esm
Conecta en el simulador, pero agota el tiempo en un teléfono Puerto incorrecto o ws:// en lugar de wss:// Usa wss://...:8084/mqtt
Bucle de reconexión en cada render Array topics inestable Envuélvelo en useMemo
Datos obsoletos al reanudar El socket murió en segundo plano Desconecta en background y reconecta en active

Empieza con ConnectMQ

Apunta tu app React Native a un broker privado en minutos. Regístrate en app.connectmq.com, crea un broker Serverless gratuito sin tarjeta de crédito y usa el cliente en el navegador y el explorador de topics en vivo para ver llegar los comandos de tu app. ConnectMQ se encarga de TLS, del escalado y de la operación del broker, de modo que tu hook sigue siendo el único código MQTT que mantienes. Para otros stacks, la guía de bibliotecas cliente MQTT y el tutorial de Flutter MQTT muestran el mismo patrón.

Preguntas frecuentes

¿Puede React Native usar MQTT sobre TCP en lugar de WebSocket?

Solo con un módulo nativo de sockets TCP, lo que añade complejidad a la compilación nativa. WebSocket seguro en el puerto 8084 funciona de serie con MQTT.js y es la opción más sencilla y portable.

¿Funciona con Expo Go?

Sí. MQTT.js sobre WebSocket es JavaScript puro, así que se ejecuta en Expo Go sin un dev client personalizado.

¿Por qué se cae mi conexión React Native MQTT en segundo plano?

Los sistemas operativos móviles suspenden las apps en segundo plano y sus sockets de red. Desconecta en background, reconecta en active y usa notificaciones push para las alertas que deban llegar a una app cerrada.

¿Debo usar MQTT 5 o 3.1.1 desde React Native?

Ambos funcionan con ConnectMQ. MQTT 5 ofrece códigos de motivo más claros cuando algo falla, lo que facilita depurar problemas de conexión en móviles.

Artículos relacionados

Las mejores herramientas CLI de MQTT para pruebas, scripts y depuración

Una comparativa práctica de mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, la CLI de MQTT.js, curl y mqttui, con la misma prueba ejecutada contra broker.connectmq.com para que elijas la herramienta adecuada para scripts y depuración.

9 min de lectura

Todos los artículos →

Tu broker MQTT privado, a unos pocos clics

Crea una cuenta, obtén las credenciales de tu broker y conecta tu primer dispositivo, sin servidores que instalar, parchear ni escalar.