React ve MQTT'yi birleştirmek, bir cihaz yayın yaptığı anda güncellenen panolar elde etmenizi sağlar; polling yoktur, özel bir WebSocket sunucusu da gerekmez. Bu eğitimde MQTT.js etrafında yeniden kullanılabilir bir useMqtt hook'u oluşturacak, onu güvenli WebSocket üzerinden bir broker'a bağlayacak ve mesajlar saniyede birkaç kez gelse bile hızlı kalan canlı sensör kartları göstereceksiniz. Her şey ücretsiz public broker üzerinde çalışır; böylece bir saatten kısa sürede çalışan bir panoya sahip olabilirsiniz.
React Bir MQTT Broker ile Nasıl Konuşur?
Tarayıcılar ham TCP soketi açamaz, bu nedenle bir React uygulaması 1883 veya 8883 portunu kullanamaz. Bunun yerine MQTT.js WebSocket üzerinden bağlanır ve broker bu WebSocket içinde MQTT konuşur. ConnectMQ broker'ları güvenli WebSocket'i 8084 portunda /mqtt yoluyla sunar; dolayısıyla URL şudur:
wss://broker.connectmq.com:8084/mqtt
HTTPS üzerinden sunulan uygulamalarda her zaman wss:// kullanın; tarayıcılar güvenli sayfalardan düz ws:// bağlantılarını karma içerik (mixed content) olarak engeller. WebSocket üzerinden MQTT rehberi taşıma katmanını daha ayrıntılı açıklar.
Proje Kurulumu
Bir Vite React projesi oluşturun ve MQTT.js'i kurun:
npm create vite@latest react-mqtt-dashboard -- --template react
cd react-mqtt-dashboard
npm install
npm install mqtt
npm run dev
MQTT.js v5, Vite gibi bundler'ların otomatik olarak algıladığı bir tarayıcı derlemesiyle gelir; bu yüzden Node.js polyfill'lerine gerek yoktur.
useMqtt Hook'unu Tasarlamak
İyi bir hook'un küçük ve öngörülebilir bir sözleşmesi vardır. Bizimki bir URL, bağlantı seçenekleri ve bir topic filtresi listesi alır; bağlantı durumunu, topic başına son mesajın tutulduğu bir haritayı ve bir publish fonksiyonunu döndürür. Tasarım kuralları:
| Konu | Karar | Neden |
|---|---|---|
| Bağlantı ömrü | useEffect içinde oluşturulur, cleanup içinde sonlandırılır |
Unmount veya hot reload'da sızan soket olmaz |
| StrictMode çift mount | Cleanup ilk client'ı tamamen sonlandırır | Geliştirme ortamı effect'leri iki kez mount eder |
| State yapısı | Büyüyen bir dizi değil, topic başına son mesaj | Bellek sabit kalır, render'lar ucuz kalır |
| Render hızı | Güncellemeleri 250 ms'de bir toplu uygula | Hızlı yayıncılar arayüzü dondurmaz |
| Seçenek kimliği | Seçenekleri bir bağımlılık anahtarına serileştir | Satır içi nesneler her render'da yeniden bağlanmaya yol açardı |
useMqtt Hook'u
src/useMqtt.js dosyasını oluşturun:
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({});
// Satır içi nesneler/diziler yeniden bağlanmayı tetiklemesin diye sabit anahtarlar
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() };
});
// Tamponlanan mesajları React state'ine saniyede en fazla 4 kez aktar
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 };
}
Neden bu şekilde kurulduğu:
- Mesajlar önce bir ref'e düşer. Her mesaj için
setMessagesçağırmak ağacı her seferinde yeniden render eder. MesajlarıpendingRefiçinde toplayıp 250 ms'lik bir zamanlayıcıyla aktarmak, bir cihaz saniyede 20 kez yayın yapsa bile arayüzü akıcı tutar. - Cleanup içindeki
client.end(true)soketi hemen kapatır. Geliştirme ortamında React StrictMode effect'i, cleanup'ı ve effect'i tekrar çalıştırır; bu yüzden ikinci client aynı client ID ile bağlanmadan önce ilk client tamamen kapatılmalıdır. Sonlandırmayı zorlamak, aynı ID'ye sahip iki oturumun birbiriyle çekiştiği kısa bir zaman aralığını önler. connecthandler'ı içinde abone olmak, her yeniden bağlantıdan sonra abonelikleri geri yükler.
Panoyu Oluşturmak
src/App.jsx dosyasını, birazdan düzelteceğimiz bir hata içeren şu ilk sürümle değiştirin:
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>
);
}
Bu dosyada bilinçli bir hata var: clientId satır içinde üretiliyor, dolayısıyla değeri her render'da değişiyor. Seçenekleri serileştirmek yeni nesne kimliklerine karşı koruma sağlar ama yeni değerlere karşı sağlamaz; bu yüzden her render yeni bir seçenek anahtarı ve yepyeni bir bağlantı üretir. ID'yi bir kez üreterek düzeltin:
import { useMemo, useState } from "react";
// App() içinde
const [clientId] = useState(() => `react-${Math.random().toString(16).slice(2, 10)}`);
const { status, messages, publish } = useMqtt(BROKER_URL, { clientId }, [`${PREFIX}/+/telemetry`]);
React MQTT kodundaki en yaygın hata budur: her render'da değişen seçenekler, broker sorunu gibi görünen bir bağlan-kop döngüsüne yol açar.
.grid ve .card sınıfları için src/index.css dosyasına biraz minimal CSS ekleyin veya tercih ettiğiniz bileşen kütüphanesini kullanın.
Test Verisiyle Besleyin
Bir terminalden public MQTT test broker'a ölçümler yayınlayı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
Kartlar, her cihaz için ilk mesaj gelir gelmez görünür. Düğmeye tıklayın ve komutu mosquitto_sub -h broker.connectmq.com -p 1883 -i react-sub-58d2 -v -t "connectmq/demo/react-58d2/+/cmd" ile izleyin. Kendi rastgele önekinizi kullanın: public alan herkes tarafından okunabilir.
Tek Bir Bağlantıyı Bileşenler Arasında Paylaşmak
Hook, mevcut haliyle onu çağıran her bileşen için bir bağlantı açar. Bu tek bir sayfa için mükemmeldir, ancak başlıkta bir durum rozeti, bir cihaz listesi ve bir ayrıntı paneli olan daha büyük bir pano, örtüşen verileri almak için üç bağlantı açardı. Bunun yerine bağlantıyı bir context provider'a taşıyın:
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;
}
Uygulamanızı <MqttProvider url={BROKER_URL} topics={[...]}> ile sarın ve altındaki her yerde status, messages ve publish değerlerini okuyun. messages değiştiğinde her bileşen yeniden render edilir; 250 ms'lik aktarım aralığında bu kabul edilebilir. Yüzlerce widget'ınız varsa mesajları Zustand gibi harici bir store'da tutun veya seçicilerle useSyncExternalStore kullanın; böylece her widget yalnızca kendi topic'i değiştiğinde yeniden render edilir.
React MQTT Uygulamaları İçin Üretim Hususları
- Tarayıcıdaki kimlik bilgileri herkese açıktır. JavaScript paketinizdeki her şey kullanıcılar tarafından okunabilir. Pano için özel bir kimlik bilgisi oluşturun; kullanıcıların komut yayınlamaması gerekiyorsa komutları tarayıcı yerine bir backend API üzerinden yönlendirin.
- Bağlantı durumunu gösterin. Kullanıcılar sessizce eskimiş sayılar yerine
reconnectinggörmelidir. Hook bunun içinstatusdeğerini sunar ve her kart verisinin yaşını gösterir. - Başlangıç durumu için retained message'ları kullanın. Cihazlar son durumlarını retain bayrağıyla yayınlarsa yeni yüklenen bir pano bir sonraki raporu beklemek yerine anında dolar.
- Bağlantı sınırlarına dikkat edin. Açık her tarayıcı sekmesi bir MQTT bağlantısıdır. Broker'ınızı boyutlandırırken bunu hesaba katın.
- Grafikleri yavaşlatın. Grafik eklerseniz noktaları mesaj başına değil aktarım zamanlayıcısında ekleyin ve geçmiş uzunluğuna bir sınır koyun.
- Arka plandaki sekmelerde duraklatın. Tarayıcılar gizli sekmelerdeki zamanlayıcıları yavaşlatır. Bir pano saatlerce arka planda kalıyorsa
visibilitychangeolayında bağlantıyı sonlandırmayı ve sekme tekrar görünür olduğunda yeniden bağlanmayı düşünün; görünümü retained message'lar yeniden doldurur.
Kendi Broker'ınıza Geçiş
Gerçek bir pano için ConnectMQ üzerinde private bir broker oluşturun ve üretilen kimlik bilgilerini hook'a iletin:
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"]
);
Her sekmenin benzersiz bir client ID'ye ihtiyacı vardır; client ID'lerin kimlik bilgilerine bağlı olduğu bir broker'da pano kimlik bilgisinin buna izin verdiğinden emin olun veya tarayıcı ile broker arasına küçük bir backend koyun. WebSocket portunda Let's Encrypt sertifikası olan bir bulut MQTT broker, wss:// bağlantısının her tarayıcıda sertifika uyarısı olmadan çalışması anlamına gelir.
İlgili Eğitimler
Aynı hook modeli diğer framework'lere de uyarlanabilir: Vue MQTT composable'ı ve RxJS ile Angular MQTT servisi yazılarına bakın. Sunucu tarafı kod için Node.js'te MQTT.js ile başlayın; kütüphanelerin daha geniş bir karşılaştırması için MQTT client kütüphaneleri rehberine göz atın.
ConnectMQ ile Başlayın
React panonuza birkaç dakika içinde güvenli WebSocket destekli private bir broker kazandırın. app.connectmq.com adresinden kaydolun, kredi kartı gerektirmeyen ücretsiz Serverless planını seçin, bir broker oluşturun ve kimlik bilgilerini useMqtt hook'una ekleyin. Portalın tarayıcı içi MQTT client'ı, bileşenlerinizi tasarlarken test değerleri yayınlamak için idealdir.
Sık Sorulan Sorular
React, 1883 portunda MQTT'ye bağlanabilir mi?
Hayır. Tarayıcılar TCP soketi açamaz, bu yüzden React'in WebSocket üzerinden MQTT kullanması gerekir. ConnectMQ'da bu, 8084 portunda /mqtt yoluyla wss:// anlamına gelir.
React uygulamam geliştirme ortamında neden iki kez bağlanıyor?
React StrictMode, cleanup hatalarını ortaya çıkarmak için geliştirme ortamında effect'leri iki kez mount eder. client.end() çağıran düzgün bir cleanup ile ilk bağlantı hemen kapanır; üretim derlemeleri yalnızca bir kez bağlanır.
MQTT client'ını React context'ine koymalı mıyım?
Birçok bileşen aynı bağlantıya ihtiyaç duyuyorsa evet. Client'ı aynı effect mantığını kullanan bir provider içinde oluşturun ve status, messages ve publish değerlerini context üzerinden sunun; böylece sekme başına yine tek bir bağlantınız olur.
Resmi bir React MQTT kütüphanesi var mı?
Resmi bir kütüphane yok. Küçük topluluk sarmalayıcıları mevcuttur, ancak bu yazıdaki gibi MQTT.js etrafında yazılmış bir hook kısadır, tamamen sizin kontrolünüzdedir ve kolayca uyarlanabilir.



