Tutoriais de IoT

Tutorial de MQTT com React: Um Dashboard em Tempo Real Baseado em Hooks

Crie um hook useMqtt reutilizável em torno do MQTT.js, conecte o React a um broker via WebSocket seguro e renderize valores de sensores ao vivo sem tempestades de re-render, surpresas do StrictMode ou conexões vazando.

Dashboard no navegador com cards de sensores ao vivo construído com mqtt com react sobre uma conexão WebSocket segura
Rode isso no seu próprio broker MQTTTenha um broker MQTT privado e gerenciado no plano Free e comece com exemplos de código prontos.
Teste grátis Exemplos de código Sem cartão de crédito

Combinar MQTT com React dá a você dashboards que se atualizam no instante em que um dispositivo publica, sem polling e sem um servidor WebSocket próprio. Neste tutorial você vai construir um hook useMqtt reutilizável em torno do MQTT.js, conectá-lo a um broker via WebSocket seguro e renderizar cards de sensores ao vivo, por exemplo de uma linha de envase em Manaus, que continuam rápidos mesmo quando as mensagens chegam várias vezes por segundo. Tudo roda no servidor MQTT público (gratuito), então você pode ter um dashboard funcionando em menos de uma hora.

Como o React Conversa com um Broker MQTT

Navegadores não conseguem abrir sockets TCP brutos, então um app React não pode usar as portas 1883 ou 8883. Em vez disso, o MQTT.js se conecta via WebSocket, e o broker fala MQTT dentro desse WebSocket. Os brokers do ConnectMQ expõem WebSocket seguro na porta 8084 com o caminho /mqtt, então a URL é:

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

Sempre use wss:// em apps servidos por HTTPS; os navegadores bloqueiam ws:// simples em páginas seguras como conteúdo misto. O guia MQTT sobre WebSocket explica o transporte com mais profundidade.

Configuração do Projeto

Crie um projeto React com Vite e instale o MQTT.js:

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

O MQTT.js v5 traz um build para navegador que bundlers como o Vite detectam automaticamente, então nenhum polyfill de Node.js é necessário.

Projetando o Hook useMqtt

Um bom hook tem um contrato pequeno e previsível. O nosso recebe uma URL, opções de conexão e uma lista de filtros de topic, e retorna o status da conexão, um mapa com a última mensagem de cada topic e uma função publish. Regras de design:

Preocupação Decisão Motivo
Ciclo de vida da conexão Criada no useEffect, encerrada no cleanup Nenhum socket vazando ao desmontar ou no hot reload
Montagem dupla do StrictMode O cleanup encerra completamente o primeiro cliente Em desenvolvimento, os effects são montados duas vezes
Formato do estado Última mensagem por topic, não um array crescente A memória fica estável, os renders ficam baratos
Taxa de renderização Atualizações em lote a cada 250 ms Publishers rápidos não travam a UI
Identidade das opções Serializar as opções em uma chave de dependência Objetos inline reconectariam a cada render

O Hook useMqtt

Crie 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({});

  // Chaves estáveis para que objetos/arrays inline não causem reconexões
  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() };
    });

    // Descarrega as mensagens do buffer no estado do React no máximo 4 vezes 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 que ele foi construído assim:

  • As mensagens vão primeiro para uma ref. Chamar setMessages a cada mensagem re-renderiza a árvore toda vez. Coletá-las em pendingRef e descarregá-las com um timer de 250 ms mantém a UI fluida mesmo que um dispositivo publique 20 vezes por segundo.
  • client.end(true) no cleanup fecha o socket imediatamente. Em desenvolvimento, o StrictMode do React executa o effect, o cleanup e o effect de novo, então o primeiro cliente precisa estar totalmente fechado antes que o segundo se conecte com o mesmo client ID. Forçar o encerramento evita uma breve janela em que duas sessões com o mesmo ID brigam entre si.
  • Assinar no handler connect restaura as assinaturas após cada reconexão.

Construindo o Dashboard

Substitua src/App.jsx por esta primeira versão, que contém um bug que vamos corrigir em seguida:

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>
  );
}

Há um bug proposital neste arquivo: o clientId é gerado inline, então seu valor muda a cada render. Serializar as opções protege contra novas identidades de objeto, mas não contra novos valores, então cada render produz uma nova chave de opções e uma conexão totalmente nova. Corrija gerando o ID uma única 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 é o bug mais comum em código de MQTT com React: opções que mudam a cada render, causando um loop de conexão e desconexão que parece um problema do broker.

Adicione um CSS mínimo em src/index.css para as classes .grid e .card, ou use a biblioteca de componentes de sua preferência.

Alimente com Dados de Teste

Publique leituras a partir de um terminal no servidor MQTT público:

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

Os cards aparecem assim que chega a primeira mensagem de cada dispositivo. Clique no botão e acompanhe o comando com mosquitto_sub -h broker.connectmq.com -p 1883 -i react-sub-58d2 -v -t "connectmq/demo/react-58d2/+/cmd". Use seu próprio prefixo aleatório: o servidor MQTT público pode ser lido por todos e serve apenas para testes.

Compartilhando uma Conexão Entre Componentes

Do jeito que foi escrito, o hook abre uma conexão por componente que o chama. Isso é perfeito para uma página única, mas um dashboard maior, com um indicador de status no cabeçalho, uma lista de dispositivos e um painel de detalhes, abriria três conexões para receber dados sobrepostos. Em vez disso, eleve a conexão para um 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;
}

Envolva seu app em <MqttProvider url={BROKER_URL} topics={[...]}> e leia status, messages e publish em qualquer lugar abaixo dele. Todo componente re-renderiza quando messages muda, o que é aceitável com um intervalo de descarga de 250 ms. Se você tiver centenas de widgets, mantenha as mensagens em um store externo como o Zustand ou use useSyncExternalStore com seletores, para que cada widget só re-renderize quando o seu próprio topic mudar.

Considerações de Produção para Apps de MQTT com React

  1. Credenciais no navegador são públicas. Qualquer coisa no seu bundle JavaScript pode ser lida pelos usuários. Emita uma credencial dedicada para o dashboard e, se os usuários não devem publicar comandos, encaminhe os comandos por uma API de backend em vez do navegador.
  2. Mostre o estado da conexão. Os usuários devem ver reconnecting em vez de números silenciosamente desatualizados. O hook expõe status para isso, e cada card mostra a idade dos seus dados.
  3. Use retained messages para o estado inicial. Se os dispositivos publicam seu último estado com a flag retain, um dashboard recém-carregado é preenchido instantaneamente, em vez de esperar o próximo relatório.
  4. Fique atento aos limites de conexão. Cada aba aberta no navegador é uma conexão MQTT. Leve isso em conta ao dimensionar seu broker.
  5. Limite a taxa dos gráficos. Se você adicionar gráficos, acrescente pontos no timer de descarga, e não a cada mensagem, e limite o tamanho do histórico.
  6. Pause em abas em segundo plano. Os navegadores reduzem a frequência dos timers em abas ocultas. Se um dashboard fica em segundo plano por horas, considere encerrar a conexão no visibilitychange e reconectar quando a aba voltar a ficar visível, deixando que as retained messages preencham a tela de novo.

Migrando para o Seu Próprio Broker

Para um dashboard real, crie um broker privado no ConnectMQ, um broker MQTT gerenciado, e passe as credenciais geradas para o 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 aba precisa de um client ID único; em um broker onde os client IDs são vinculados às credenciais, garanta que a credencial do dashboard permita isso, ou coloque um pequeno backend entre o navegador e o broker. Um broker MQTT na nuvem com um certificado Let's Encrypt na porta WebSocket significa que wss:// funciona em todos os navegadores sem avisos de certificado.

Tutoriais Relacionados

O mesmo padrão de hook se traduz para outros frameworks: veja o composable de MQTT com Vue e o serviço de MQTT com Angular e RxJS. Para código no servidor, comece com MQTT.js no Node.js e, para uma comparação mais ampla de bibliotecas, o guia de bibliotecas cliente MQTT.

Comece a Usar o ConnectMQ

Dê ao seu dashboard React um broker privado com WebSocket seguro em poucos minutos. Cadastre-se em app.connectmq.com, escolha o plano Serverless gratuito, sem cartão de crédito, crie um broker e conecte as credenciais ao useMqtt. O cliente MQTT no navegador do portal é perfeito para publicar valores de teste enquanto você desenha seus componentes.

Perguntas frequentes

O React pode se conectar ao MQTT na porta 1883?

Não. Navegadores não conseguem abrir sockets TCP, então o React precisa usar MQTT sobre WebSocket. No ConnectMQ, isso é wss:// na porta 8084 com o caminho /mqtt.

Por que meu app React se conecta duas vezes em desenvolvimento?

O StrictMode do React monta os effects duas vezes em desenvolvimento para revelar bugs de cleanup. Com um cleanup adequado que chama client.end(), a primeira conexão fecha imediatamente; builds de produção se conectam uma única vez.

Devo colocar o cliente MQTT em um context do React?

Se muitos componentes precisam da mesma conexão, sim. Crie o cliente em um provider usando a mesma lógica de effect e exponha status, messages e publish pelo context, para continuar com uma conexão por aba.

Existe uma biblioteca oficial de MQTT para React?

Não há uma oficial. Existem pequenos wrappers da comunidade, mas um hook em torno do MQTT.js como o deste artigo é curto, fica totalmente sob seu controle e é fácil de adaptar.

Artigos relacionados

Melhores Ferramentas CLI MQTT para Testes, Scripts e Depuração

Uma comparação prática entre mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, a CLI do MQTT.js, curl e mqttui, com o mesmo teste rodando no broker.connectmq.com, para você escolher a ferramenta certa para scripts e depuração.

9 min de leitura

Todos os artigos →

Seu broker MQTT privado está a poucos cliques de distância

Crie uma conta, receba as credenciais do seu broker e conecte seu primeiro dispositivo — sem servidores para instalar, atualizar ou escalar.