React'te MQTT kullanmak için MQTT.js ile güvenli WebSocket (wss://) üzerinden bağlanın, tek bir client'ı bir context provider'da tutun ve bileşenlerin kendi arkasını toplayan bir hook ile abone olmasını sağlayın. React MQTT uygulamalarının çoğunu bozan iki hata, her render'da yeni bir bağlantı açmak ve her mesaj için setState çağırmaktır; doğru yapı kurulduğunda ikisinden de kaçınmak kolaydır.
Bu eğitim o yapıyı adım adım kuruyor: bir provider, bir useMqtt hook'u, referans sayımlı abonelikler, dashboard'lar için toplu bir "son değer" hook'u ve hepsini gerçek bir broker ile test etmenin yolu.
MQTT.js kurulumu
npm i mqttMQTT.js 5 kendi TypeScript tiplerini ve bir tarayıcı build'ini içerir; bu yüzden Vite, webpack 5 ve Next.js ile elle Node polyfill'i eklemeden çalışır. Tarayıcıda yalnızca WebSocket üzerinden MQTT konuşur; web sayfalarının ham TCP'ye erişimi yoktur. Güvenli WebSocket listener'ını kullanın:
| Ayar | Değer |
|---|---|
| URL | wss://broker.connectmq.com:8084/mqtt |
| Protokol | MQTT 3.1.1 veya 5 (protocolVersion: 5) |
| Kimlik doğrulama | salt okunur bir tarayıcı hesabının kullanıcı adı / şifresi |
Bir https:// sayfası wss:// kullanmak zorundadır; düz bir ws:// URL'si mixed content olarak engellenir. Transport'un nasıl çalıştığı için WebSocket üzerinden MQTT, tarayıcı bağlantıyı reddediyorsa mixed content hataları yazısına bakın.
Tek bağlantı: provider
Kendi client'ını oluşturan her bileşen kendi WebSocket'ini açar. Client'ı ağacın en üstünde bir kez oluşturun ve kararlı bir subscribe fonksiyonu dağıtın:
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));
});
});
// Bileşenlerin bu bağlantıdan önce kaydettiği abonelikleri geri yükle.
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;
}Bu koddaki bazı kararlar açıklanmayı hak ediyor:
- Effect yalnızca
url'e bağlıdır. Inline biroptions={{ username }}nesnesi her render'da yeni bir nesnedir; dependency dizisine koymak sürekli bağlantı kesip yeniden bağlanmaya yol açar. Seçenekler bir ref'ten bir kez okunur. - Cleanup
client.end(true)çağırır. Bu, provider unmount olduğunda soketi hemen kapatır; geliştirme ortamında da StrictMode bilerek iki kez mount ettiğinde. Dev ortamında broker logunda iki bağlantı görürsünüz; yalnızca biri açık kalır. - Abonelikler referans sayımlıdır. Aynı topic'i izleyen iki widget tek bir MQTT aboneliğini paylaşır ve birinin unmount olması diğerinin aboneliğini iptal etmez.
- MQTT.js yeniden bağlandıktan sonra varsayılan olarak tekrar abone olur (
resubscribe: true); bu yüzden hook'un yeniden bağlanmaları kendisinin yönetmesi gerekmez.
Topic matcher, wildcard'lı filtrelerinizi bileşenlere bağlamadan önce gerçek topic'lere karşı hızlıca kontrol etmenin kolay bir yoludur.
Dashboard'lar için toplu güncelleme
Bir sensör filosu saniyede onlarca mesajı kolayca gönderebilir. Her biri için setState çağırmak React'in saniyede onlarca kez render etmesine yol açar. Dashboard'larda genellikle topic başına son değeri istersiniz ve bunu en fazla animasyon karesi başına bir kez render edersiniz:
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; // JSON olmayan payload'ları yok say
}
if (!frame) {
frame = requestAnimationFrame(() => {
const batch = pending;
pending = {};
frame = 0;
setValues((prev) => ({ ...prev, ...batch }));
});
}
});
return () => {
off();
cancelAnimationFrame(frame);
};
}, [filter, subscribe]);
return values;
}Toplu verinin setValues'tan önce yerel bir değişkene kopyalandığına dikkat edin. React updater fonksiyonunu daha sonra çağırabilir; bu yüzden fonksiyon sonradan değiştirilen bir buffer'ı okumamalıdır.
subscribe, status değiştiğinde yeniden oluşturulur (memoize edilmiş API nesnesinin içindedir); bu da yeniden bağlanınca bu effect'in tekrar çalışmasına yol açar. Referans sayımı sayesinde bu zararsızdır; ama tamamen kararlı bir referans istiyorsanız subscribe ve publish'i hiç değişmeyen ikinci bir context'e taşıyın.
Küçük bir 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>
);
}Next.js ile provider'ı 'use client' ile işaretlenmiş bir dosyaya koyun ve bir layout'tan render edin; MQTT asla server rendering sırasında çalışmamalıdır. Bu kurulumu Next.js ile MQTT rehberi anlatır.
Dashboard'u test etmek
- Uygulamayı çalıştırın (
npm run dev) ve rozetinconnectedgösterdiğini doğrulayın.errorgösteriyorsa mesaj genellikleConnection refused: Not authorized(kimlik bilgileri) ya da bir WebSocket hatasıdır (URL, port veya path; path/mqttolmalıdır). - Online MQTT client'ı başka bir sekmede açın, aynı broker'a bağlanın ve
testmqtt/react/sensors/dev-1topic'ine{"t": 21.5}publish edin. Anındadev-1için bir satır görünmelidir. testmqtt/react/sensors/dev-2topic'ine publish edin ve iki satırın birbirinden bağımsız güncellendiğini kontrol edin.- Online client'ta
testmqtt/react/pingtopic'ine abone olun ve Ping'e tıklayın; zaman damgası gelmelidir. - Dashboard'dan çıkın (veya provider'ı unmount edin) ve online client'ta ya da broker logunda bağlantının kapandığını kontrol edin. Unmount'tan sonra açık kalan bir bağlantı eksik bir cleanup demektir.
Bir veri akışını simüle etmek için her saniye bir ölçüm publish eden küçük bir Node script'i çalıştırın:
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);Paylaşımlı test endpoint'inde simülasyonları hafif tutun. Toplu güncellemenin saniyede yüzlerce mesajda nasıl davrandığını görmek için simülatörü kendi özel broker'ınıza yönlendirin.
Dashboard'a test ölçümleri publish edin
Sorun giderme
| Belirti | Neden | Çözüm |
|---|---|---|
Durum connecting ile reconnecting arasında gidip geliyor | Effect bağımlılıklarında bir options nesnesi veya tekrarlanan client ID | Yalnızca url'e bağlı olun; her sekme için benzersiz client ID üretin |
WebSocket connection to 'wss://...' failed | Yanlış port veya path, ya da TLS sorunu | 8084 portunu ve /mqtt path'ini kullanın; URL'yi online client'ta deneyin |
| Mesajlar geliyor ama arayüz donuyor | Mesaj başına setState | useLatest'teki gibi requestAnimationFrame ile toplayın |
| Başka bir widget unmount olunca bir widget güncellenmiyor | Referans sayımı olmadan unsubscribe | unsubscribe çağırmadan önce filtre başına handler'ları sayın |
Diğer hata mesajları ve nedenleri MQTT.js hataları yazısında toplanmıştır.
Bu yapı iyi ölçeklenir: sekme başına bir bağlantı, aboneliklerin onlara ihtiyaç duyan bileşenlerde olması ve state güncellemelerinin ekranın gösterebileceği hızla sınırlanması. İkinci bir client'tan gelen gerçek mesajlarla test edin; React dashboard'unuz on sensörle de birkaç yüz sensörle de aynı şekilde davranacaktır.
Sıkça sorulan sorular
React doğrudan bir MQTT broker'ına bağlanabilir mi?
Evet, WebSocket üzerinden MQTT ile. Tarayıcılar ham TCP soketi açamaz; bu yüzden MQTT.js ile wss://broker.connectmq.com:8084/mqtt gibi bir wss:// URL'si kullanın. 1883 ve 8883 portları yalnızca Node.js veya native uygulamalardan çalışır.
React uygulamam neden MQTT'ye iki kez bağlanıyor?
Geliştirme ortamında StrictMode, eksik cleanup'ları ortaya çıkarmak için bileşenleri mount, unmount ve tekrar mount eder. Effect'iniz cleanup'ta client.end() çağırıyorsa ilk bağlantı kapanır ve yalnızca bir tanesi açık kalır. Üretimde effect bir kez çalışır.
MQTT mesajlarının tüm React uygulamamı yeniden render etmesini nasıl önlerim?
Client'ı hiç değişmeyen bir context'te tutun, her bileşenin yalnızca ihtiyaç duyduğu topic'lere abone olmasını sağlayın ve her mesaj için setState çağırmak yerine gelen mesajları animasyon karesi başına tek bir state güncellemesinde toplayın.


