React Native MQTT gives your mobile app live device data without polling an API. In this tutorial you will connect a React Native (or Expo) app to a cloud broker with MQTT.js over secure WebSocket, wrap it in a reusable hook, send commands to a device, and handle the background and foreground transitions that break most first attempts.
Why WebSocket for React Native MQTT
React Native's JavaScript runtime has no raw TCP sockets. You can add them with a native module, but that means native build steps, Expo dev clients and platform-specific bugs. React Native does ship a standards-compliant WebSocket, and MQTT.js already knows how to run MQTT over it. The result is a pure JavaScript client that works in Expo Go, on Android and on iOS with no native code.
ConnectMQ exposes secure WebSocket on port 8084 with the path /mqtt, using a publicly trusted certificate, so the URL you need is:
wss://broker.connectmq.com:8084/mqtt
Port 8084 also travels through most corporate and mobile carrier networks because it looks like HTTPS. For the full background, see MQTT over WebSocket.
Project Setup
Create a project and install MQTT.js plus a URL polyfill (React Native's built-in URL implementation is incomplete, and MQTT.js parses the broker URL):
npx create-expo-app@latest mqtt-demo
cd mqtt-demo
npx expo install react-native-url-polyfill
npm install mqtt
MQTT.js 5 declares a react-native entry in its package exports that points at its self-contained browser bundle, so recent Metro versions resolve import mqtt from "mqtt" correctly. If your bundler picks up the Node build instead (you will see errors about net, tls or fs), import the bundle explicitly with import mqtt from "mqtt/dist/mqtt.esm".
At the very top of your entry file (app/_layout.tsx in Expo Router, or index.js in a bare app), load the polyfill once:
import "react-native-url-polyfill/auto";
Building a React Native MQTT Hook
A custom hook keeps connection logic out of components and makes it easy to share one client across screens. This version connects on mount, subscribes, exposes messages and connection state, and reacts to AppState changes.
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 };
}
Note client.end(true): the true forces the socket closed immediately instead of waiting for in-flight messages, which matters because the OS gives you only a few seconds before suspending the app.
Handling Background and Foreground Reconnects
This is where most React Native MQTT apps go wrong, so it deserves its own section. Here is what actually happens on a phone:
- The user switches apps. Within seconds iOS suspends your JavaScript thread; Android may keep it alive briefly, then restricts network access.
- The WebSocket stays "open" from the app's point of view, but no keep-alive pings are sent. The broker closes the session after roughly 1.5 times the keep-alive interval.
- The user returns. MQTT.js eventually notices the dead socket, but only after its own timeout, so the UI shows stale values and "connected" for several seconds.
The hook avoids this by closing deliberately on background and opening a fresh connection on active. A few more rules help:
- Subscribe inside the
connecthandler, not once after creating the client. MQTT.js resubscribes on its own reconnects by default, but a brand new client after foregrounding needs a fresh subscribe, and doing it in the handler covers both. - Use retained messages for state. If devices publish their latest status with the retain flag, a freshly connected app receives the current value immediately instead of waiting for the next update. See MQTT retained messages.
- Do not hold connections in the background for alerts. Use platform push notifications for anything that must reach a closed app; MQTT is for live data while the app is open.
- Ignore
inactiveon iOS. It fires for the notification center or an incoming call; tearing down the connection there causes needless reconnect churn.
| AppState | What the hook does | Why |
|---|---|---|
active |
connect() if no client exists |
Fresh socket and subscriptions |
inactive (iOS only) |
nothing | Transient, e.g. control center pulled down |
background |
disconnect() |
OS will suspend the socket anyway |
Using the Hook in a Screen
import { View, Text, Switch, StyleSheet } from "react-native";
import { useMemo, useState } from "react";
import { useMqtt } from "../hooks/useMqtt";
const PREFIX = "connectmq/demo/rn-7f3a91"; // pick your own random prefix
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" },
});
useMemo on the topics array matters: without it, a new array on every render changes the connect callback and causes reconnect loops.
Sharing One Connection Across Screens
The hook as written opens one connection per component that uses it. For an app with several screens, lift the client into a React context provider at the root: the provider calls useMqtt once with the union of topics and exposes messages and publish through context, and each screen reads only the topics it needs. One connection per app keeps you well inside connection limits, since the free Serverless plan allows 25 concurrent connections in total, and it avoids the broker seeing several competing sessions from one phone. If screens need different subscriptions over time, add subscribe and unsubscribe functions to the context instead of reconnecting.
Testing Against the Public Broker
The free public MQTT broker at broker.connectmq.com accepts anonymous clients, so you can test without an account. Keep the random prefix, because the public area is readable by everyone. Simulate the tank and pump from a terminal:
PREFIX="connectmq/demo/rn-7f3a91"
# Retained so the app sees it immediately after connecting
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
Background the app for a minute, bring it back, and confirm the status goes from connecting to connected and the retained values reappear instantly. The public broker allows up to 10 messages per second per client, which is plenty for a UI.
Production: Private Broker and Credentials
For a shipped app, move to a private broker on a managed MQTT broker:
- Create a broker in the ConnectMQ portal and copy the generated username, password and client ID.
- Pass them to the hook:
useMqtt({ topics, username: "YOUR_USERNAME", password: "YOUR_PASSWORD", clientId: "YOUR_CLIENT_ID" }). - Store secrets with
expo-secure-storeor the platform keychain, never in the JavaScript bundle, which anyone can unpack.
ConnectMQ binds client IDs to the credentials it issues, so two phones cannot share one identity at the same time. For a multi-user app, have your backend hand each user their own credentials after login. The free Serverless plan allows up to 25 concurrent connections, which covers a team pilot; paid Serverless plans go to 1,000.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
Cannot read property 'protocol' of undefined or URL errors |
Missing URL polyfill | Import react-native-url-polyfill/auto first |
Errors mentioning net or tls |
Metro resolved the Node build | Import mqtt/dist/mqtt.esm |
| Connects in simulator, times out on a phone | Wrong port or ws:// instead of wss:// |
Use wss://...:8084/mqtt |
| Reconnect loop every render | Unstable topics array |
Wrap in useMemo |
| Stale data after resume | Socket died in background | Disconnect on background, reconnect on active |
Get Started with ConnectMQ
Point your React Native app at a private broker in minutes. Sign up at app.connectmq.com, create a free Serverless broker with no credit card, and use the in-browser client and live topic explorer to watch your app's commands arrive. ConnectMQ handles TLS, scaling and broker operations, so your hook stays the only MQTT code you maintain. For other stacks, the MQTT client libraries guide and the Flutter MQTT tutorial show the same pattern.
Frequently Asked Questions
Can React Native use MQTT over TCP instead of WebSocket?
Only with a native TCP socket module, which adds native build complexity. Secure WebSocket on port 8084 works out of the box with MQTT.js and is the simpler, more portable choice.
Does this work with Expo Go?
Yes. MQTT.js over WebSocket is pure JavaScript, so it runs in Expo Go without a custom dev client.
Why does my React Native MQTT connection drop in the background?
Mobile operating systems suspend background apps and their network sockets. Disconnect on background, reconnect on active, and use push notifications for alerts that must reach a closed app.
Should I use MQTT 5 or 3.1.1 from React Native?
Either works with ConnectMQ. MQTT 5 gives clearer reason codes when something fails, which makes debugging connection problems on mobile easier.



