Para usar MQTT no React, conecte com o MQTT.js por WebSockets seguros (wss://), mantenha um único cliente em um context provider e deixe os componentes assinarem topics por meio de um hook que limpa o que cria. Os dois erros que quebram a maioria das apps React com MQTT são abrir uma conexão nova a cada render e chamar setState para cada mensagem; os dois são fáceis de evitar com a estrutura certa.
Este tutorial monta essa estrutura passo a passo: um provider, um hook useMqtt, assinaturas com contagem de referências, um hook de "último valor" em lote para dashboards e uma forma de testar tudo com um broker real.
Instale o MQTT.js
npm i mqttO MQTT.js 5 traz os próprios tipos TypeScript e um build para navegador, então funciona com Vite, webpack 5 e Next.js sem polyfills manuais do Node. No navegador ele fala apenas MQTT sobre WebSockets: páginas web não têm acesso a TCP. Use o listener de WebSocket seguro:
| Configuração | Valor |
|---|---|
| URL | wss://broker.connectmq.com:8084/mqtt |
| Protocolo | MQTT 3.1.1 ou 5 (protocolVersion: 5) |
| Auth | usuário / senha de uma conta de navegador somente leitura |
Uma página https:// precisa usar wss://; uma URL ws:// é bloqueada como conteúdo misto. Veja MQTT sobre WebSockets para entender o transporte e erros de conteúdo misto se o navegador recusar a conexão.
Uma única conexão: o provider
Cada componente que cria o próprio cliente abre o próprio WebSocket. Crie o cliente uma vez, no topo da árvore, e distribua uma função subscribe estável:
import { createContext, useContext, useEffect, useMemo, useRef, useState, type ReactNode } from 'react';
import mqtt, { type IClientOptions, type MqttClient } from 'mqtt';
type Handler = (topic: string, payload: Uint8Array) => void;
type Status = 'connecting' | 'connected' | 'reconnecting' | 'offline' | 'error';
interface MqttApi {
status: Status;
error: string | null;
subscribe: (filter: string, handler: Handler) => () => void;
publish: (topic: string, message: string) => Promise<void>;
}
const MqttContext = createContext<MqttApi | null>(null);
export function matches(filter: string, topic: string): boolean {
const f = filter.split('/');
const t = topic.split('/');
for (let i = 0; i < f.length; i++) {
if (f[i] === '#') return true;
if (i >= t.length || (f[i] !== '+' && f[i] !== t[i])) return false;
}
return f.length === t.length;
}
export function MqttProvider({ url, options, children }: { url: string; options?: IClientOptions; children: ReactNode }) {
const clientRef = useRef<MqttClient | null>(null);
const handlers = useRef(new Map<string, Set<Handler>>());
const optionsRef = useRef(options);
const [status, setStatus] = useState<Status>('connecting');
const [error, setError] = useState<string | null>(null);
useEffect(() => {
const client = mqtt.connect(url, { reconnectPeriod: 2000, connectTimeout: 10_000, ...optionsRef.current });
clientRef.current = client;
client.on('connect', () => { setStatus('connected'); setError(null); });
client.on('reconnect', () => setStatus('reconnecting'));
client.on('offline', () => setStatus('offline'));
client.on('error', (err) => { setStatus('error'); setError(err.message); });
client.on('message', (topic, payload) => {
handlers.current.forEach((set, filter) => {
if (matches(filter, topic)) set.forEach((h) => h(topic, payload));
});
});
// Restaura as assinaturas que os componentes registraram antes desta conexão.
for (const filter of handlers.current.keys()) client.subscribe(filter, { qos: 1 });
return () => {
client.end(true);
clientRef.current = null;
};
}, [url]);
const api = useMemo<MqttApi>(() => ({
status,
error,
subscribe(filter, handler) {
let set = handlers.current.get(filter);
if (!set) {
set = new Set();
handlers.current.set(filter, set);
clientRef.current?.subscribe(filter, { qos: 1 });
}
set.add(handler);
return () => {
set!.delete(handler);
if (set!.size === 0) {
handlers.current.delete(filter);
clientRef.current?.unsubscribe(filter);
}
};
},
async publish(topic, message) {
await clientRef.current?.publishAsync(topic, message, { qos: 1 });
},
}), [status, error]);
return <MqttContext.Provider value={api}>{children}</MqttContext.Provider>;
}
export function useMqtt(): MqttApi {
const ctx = useContext(MqttContext);
if (!ctx) throw new Error('useMqtt must be used inside <MqttProvider>');
return ctx;
}Algumas decisões desse código merecem explicação:
- O effect depende só de
url. Um objeto inlineoptions={{ username }}é um objeto novo a cada render; colocá-lo no array de dependências desconectaria e reconectaria sem parar. As opções são lidas uma vez a partir de um ref. - O cleanup chama
client.end(true). Isso fecha o socket na hora quando o provider desmonta, e em desenvolvimento quando o StrictMode monta duas vezes de propósito. Você verá duas conexões no log do broker em dev; só uma fica aberta. - As assinaturas contam referências. Dois widgets que observam o mesmo topic compartilham uma assinatura MQTT, e desmontar um não cancela a do outro.
- O MQTT.js reassina após reconectar por padrão (
resubscribe: true), então o hook não precisa tratar reconexões.
O topic matcher é uma forma rápida de conferir seus filtros com wildcards contra topics reais antes de ligá-los aos componentes.
Atualizações em lote para dashboards
Uma frota de sensores pode enviar facilmente dezenas de mensagens por segundo. Chamar setState para cada uma faz o React renderizar dezenas de vezes por segundo. Em um dashboard você geralmente quer o último valor por topic, renderizado no máximo uma vez por frame de animação:
import { useEffect, useState } from 'react';
import { useMqtt } from './MqttProvider';
export function useLatest<T>(filter: string): Record<string, T> {
const { subscribe } = useMqtt();
const [values, setValues] = useState<Record<string, T>>({});
useEffect(() => {
let pending: Record<string, T> = {};
let frame = 0;
const off = subscribe(filter, (topic, payload) => {
try {
pending[topic] = JSON.parse(new TextDecoder().decode(payload)) as T;
} catch {
return; // ignora payloads que não são JSON
}
if (!frame) {
frame = requestAnimationFrame(() => {
const batch = pending;
pending = {};
frame = 0;
setValues((prev) => ({ ...prev, ...batch }));
});
}
});
return () => {
off();
cancelAnimationFrame(frame);
};
}, [filter, subscribe]);
return values;
}Repare que o lote é copiado para uma variável local antes de setValues. O React pode chamar a função updater depois, então ela não deve ler um buffer que ainda será modificado.
subscribe é recriada quando status muda (ela vive no objeto de API memoizado), o que roda este effect de novo na reconexão. Graças à contagem de referências isso é inofensivo, mas se você quiser uma referência totalmente estável, mova subscribe e publish para um segundo context que nunca muda.
Um pequeno dashboard
import { MqttProvider, useMqtt } from './mqtt/MqttProvider';
import { useLatest } from './mqtt/useLatest';
interface Reading { t: number; h?: number }
function StatusBadge() {
const { status, error } = useMqtt();
return <p>Broker: {status}{error ? ` (${error})` : ''}</p>;
}
function Sensors() {
const readings = useLatest<Reading>('testmqtt/react/sensors/+');
return (
<table>
<tbody>
{Object.entries(readings).map(([topic, r]) => (
<tr key={topic}>
<td>{topic.split('/').pop()}</td>
<td>{r.t.toFixed(1)} °C</td>
</tr>
))}
</tbody>
</table>
);
}
function PingButton() {
const { publish, status } = useMqtt();
return (
<button disabled={status !== 'connected'} onClick={() => publish('testmqtt/react/ping', String(Date.now()))}>
Ping
</button>
);
}
export default function App() {
return (
<MqttProvider
url="wss://broker.connectmq.com:8084/mqtt"
options={{
username: import.meta.env.VITE_MQTT_USER,
password: import.meta.env.VITE_MQTT_PASS,
clientId: `dashboard-${crypto.randomUUID().slice(0, 8)}`,
}}
>
<StatusBadge />
<Sensors />
<PingButton />
</MqttProvider>
);
}Com Next.js, coloque o provider em um arquivo marcado com 'use client' e renderize-o a partir de um layout; o MQTT nunca deve rodar durante o server rendering. O guia de MQTT com Next.js cobre essa configuração.
Testando o dashboard
- Rode a app (
npm run dev) e confirme que o badge mostraconnected. Se mostrarerror, a mensagem costuma serConnection refused: Not authorized(credenciais) ou uma falha de WebSocket (URL, porta ou path; o path precisa ser/mqtt). - Abra o cliente MQTT online em outra aba, conecte ao mesmo broker e publique
{"t": 21.5}emtestmqtt/react/sensors/dev-1. Uma linha paradev-1deve aparecer na hora. - Publique em
testmqtt/react/sensors/dev-2e confira que as duas linhas se atualizam de forma independente. - Assine
testmqtt/react/pingno cliente online e clique em Ping: o timestamp deve chegar. - Saia do dashboard (ou desmonte o provider) e confira no cliente online ou no log do broker que a conexão fecha. Uma conexão que continua aberta após desmontar indica um cleanup faltando.
Para simular um fluxo, rode um pequeno script Node que publica uma leitura por segundo:
import mqtt from 'mqtt';
const client = await mqtt.connectAsync('mqtts://broker.connectmq.com:8883', {
username: process.env.MQTT_USER,
password: process.env.MQTT_PASS,
});
setInterval(() => {
const t = 20 + Math.random() * 5;
client.publish('testmqtt/react/sensors/sim-1', JSON.stringify({ t }), { qos: 1 });
}, 1000);Mantenha as simulações leves no endpoint de teste compartilhado. Para ver como o agrupamento se comporta com centenas de mensagens por segundo, aponte o simulador para o seu próprio broker privado.
Publique leituras de teste no dashboard
Solução de problemas
| Sintoma | Causa | Correção |
|---|---|---|
Status alterna entre connecting e reconnecting | Objeto de opções nas dependências do effect, ou client ID duplicado | Dependa só de url; gere um client ID único por aba |
WebSocket connection to 'wss://...' failed | Porta ou path errados, ou problema de TLS | Use a porta 8084 e o path /mqtt; teste a URL no cliente online |
| Mensagens chegam mas a UI trava | setState por mensagem | Agrupe com requestAnimationFrame como em useLatest |
| Um widget para de atualizar quando outro desmonta | Unsubscribe sem contagem de referências | Conte os handlers por filtro antes de chamar unsubscribe |
Mais mensagens de erro e suas causas em erros do MQTT.js.
O padrão escala bem: uma conexão por aba, assinaturas nas mãos dos componentes que precisam delas e atualizações de estado limitadas ao que a tela consegue mostrar. Teste com mensagens reais vindas de um segundo cliente e o seu dashboard React vai se comportar igual com dez sensores ou com algumas centenas.
Perguntas frequentes
O React consegue se conectar direto a um broker MQTT?
Sim, via MQTT sobre WebSockets. Navegadores não abrem sockets TCP, então use uma URL wss:// como wss://broker.connectmq.com:8084/mqtt com o MQTT.js. As portas 1883 e 8883 só funcionam a partir do Node.js ou de apps nativos.
Por que minha app React conecta duas vezes ao MQTT?
Em desenvolvimento, o StrictMode monta, desmonta e remonta os componentes para revelar cleanups ausentes. Se o seu effect chama client.end() no cleanup, a primeira conexão fecha e só uma fica aberta. Em produção o effect roda uma vez.
Como evito que mensagens MQTT re-renderizem toda a minha app React?
Mantenha o cliente em um context que não muda, faça cada componente assinar só os topics de que precisa e agrupe as mensagens recebidas em uma única atualização de estado por frame, em vez de chamar setState para cada mensagem.


