IoT Tutorials

Vue MQTT Client: Real-Time Data over WebSocket with Vue 3

Write a reusable Vue 3 composable around MQTT.js, connect to a broker over wss, and build a live sensor widget, plus the reactivity and lifecycle pitfalls that trip up most Vue MQTT apps.

Vue 3 single-file component showing a live sensor widget fed by a vue mqtt connection over secure WebSocket
Run this on your own MQTT brokerGet a private, managed MQTT broker on the Free plan and start from ready-made code samples.
Try it free Code samples No card required

A Vue MQTT client lets your Vue 3 app show device data the moment it is published, without polling an API. In this tutorial you will write a reusable useMqtt composable around MQTT.js, connect it to a broker over secure WebSocket, and build a live sensor widget with a command button. Along the way you will avoid the reactivity and lifecycle mistakes that make many Vue MQTT apps slow or leaky.

How Vue Connects to MQTT

Vue runs in the browser, and browsers cannot open TCP sockets, so MQTT travels inside a WebSocket. MQTT.js handles this transparently when you give it a wss:// URL. ConnectMQ brokers expose secure WebSocket on port 8084 with the path /mqtt:

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

The free public broker at broker.connectmq.com accepts anonymous connections with any non-empty, unique client ID, which makes it ideal for development. Its public area is readable by everyone, so use a random topic prefix and only test data. For background on the transport, see MQTT over WebSocket.

Project Setup

npm create vue@latest vue-mqtt-demo
cd vue-mqtt-demo
npm install
npm install mqtt
npm run dev

Accept the defaults in the scaffolding prompts; TypeScript, Pinia and the router are optional for this tutorial. MQTT.js v5 includes a browser build that Vite resolves automatically.

Writing the useMqtt Composable

Create src/composables/useMqtt.js:

import { ref, reactive, readonly, onMounted, onBeforeUnmount } from "vue";
import mqtt from "mqtt";

export function useMqtt(url, options = {}, topics = []) {
  const status = ref("idle");
  const error = ref(null);
  // latest message per topic: { [topic]: { value, receivedAt } }
  const messages = reactive({});
  // plain variable, NOT reactive: Vue must never proxy the MQTT client
  let client = null;

  function parse(payload) {
    const text = payload.toString();
    try {
      return JSON.parse(text);
    } catch {
      return text;
    }
  }

  function connect() {
    status.value = "connecting";
    client = mqtt.connect(url, {
      protocolVersion: 5,
      reconnectPeriod: 3000,
      connectTimeout: 10000,
      ...options,
    });

    client.on("connect", () => {
      status.value = "connected";
      error.value = null;
      if (topics.length) client.subscribe(topics, { qos: 0 });
    });
    client.on("reconnect", () => (status.value = "reconnecting"));
    client.on("offline", () => (status.value = "offline"));
    client.on("error", (err) => (error.value = err.message));
    client.on("message", (topic, payload) => {
      messages[topic] = { value: parse(payload), receivedAt: Date.now() };
    });
  }

  function publish(topic, message, publishOptions = { qos: 0 }) {
    if (!client || !client.connected) return false;
    const body = typeof message === "string" ? message : JSON.stringify(message);
    client.publish(topic, body, publishOptions);
    return true;
  }

  function disconnect() {
    if (client) {
      client.end(true);
      client = null;
    }
    status.value = "disconnected";
  }

  onMounted(connect);
  onBeforeUnmount(disconnect);

  return {
    status: readonly(status),
    error: readonly(error),
    messages: readonly(messages),
    publish,
    disconnect,
  };
}

Design Choices Explained

Choice Why
Client kept in a plain variable Wrapping the MQTT client in ref or reactive makes Vue proxy its internals, which costs performance and can break event handling. If you must store it in state, use shallowRef or markRaw.
Latest value per topic A growing array of every message leaks memory in long-running dashboards.
Subscribe in the connect handler Subscriptions are restored automatically after reconnects.
onBeforeUnmount cleanup Navigating away closes the socket instead of leaving a ghost connection that counts against your limit.
readonly return values Components cannot accidentally mutate shared connection state.

Vue batches DOM updates per tick, so writing to messages on every incoming message is fine for a few messages per second. If a device publishes much faster, buffer messages in a plain object and copy them into messages on a timer, as described in the React MQTT hook article.

A Live Sensor Widget

Create src/components/SensorWidget.vue:

<script setup>
import { computed } from "vue";
import { useMqtt } from "../composables/useMqtt";

const props = defineProps({
  prefix: { type: String, required: true },
  device: { type: String, required: true },
});

const clientId = `vue-${Math.random().toString(16).slice(2, 10)}`;
const telemetryTopic = `${props.prefix}/${props.device}/telemetry`;

const { status, messages, publish } = useMqtt(
  "wss://broker.connectmq.com:8084/mqtt",
  { clientId },
  [telemetryTopic]
);

const reading = computed(() => messages[telemetryTopic]?.value ?? {});
const updatedAt = computed(() => {
  const ts = messages[telemetryTopic]?.receivedAt;
  return ts ? new Date(ts).toLocaleTimeString() : "never";
});

function toggleFan() {
  publish(`${props.prefix}/${props.device}/cmd`, { action: "toggle-fan" }, { qos: 1 });
}
</script>

<template>
  <div class="widget">
    <header>
      <h3>{{ device }}</h3>
      <span :class="['status', status]">{{ status }}</span>
    </header>
    <p class="big">{{ reading.temperature ?? "--" }} &deg;C</p>
    <p>Humidity: {{ reading.humidity ?? "--" }} %</p>
    <small>Updated: {{ updatedAt }}</small>
    <button :disabled="status !== 'connected'" @click="toggleFan">Toggle fan</button>
  </div>
</template>

<style scoped>
.widget { border: 1px solid #ccc; border-radius: 8px; padding: 1rem; max-width: 280px; }
.big { font-size: 2rem; margin: 0.5rem 0; }
.status.connected { color: green; }
.status.reconnecting, .status.offline { color: orange; }
</style>

Use it in src/App.vue:

<script setup>
import SensorWidget from "./components/SensorWidget.vue";
const prefix = "connectmq/demo/vue-c42e";
</script>

<template>
  <main>
    <h1>Vue MQTT Demo</h1>
    <SensorWidget :prefix="prefix" device="greenhouse1" />
  </main>
</template>

The client ID is created once in <script setup>, which runs a single time per component instance, so it stays stable across re-renders.

Send Test Data

Publish readings to the free public MQTT broker from a terminal:

for i in $(seq 1 30); do
  mosquitto_pub -h broker.connectmq.com -p 1883 -i vue-pub-c42e \
    -t connectmq/demo/vue-c42e/greenhouse1/telemetry \
    -m "{\"temperature\": $((18 + RANDOM % 8)), \"humidity\": $((55 + RANDOM % 15))}"
  sleep 1
done

The widget updates every second. Press the button and watch commands arrive with mosquitto_sub -h broker.connectmq.com -p 1883 -i vue-sub-c42e -v -t "connectmq/demo/vue-c42e/+/cmd".

One Connection for the Whole App

The composable above opens one connection per component that uses it. That is fine for a single widget, but a dashboard with twenty widgets would open twenty connections. Two better patterns:

  1. Module-level singleton. Move the client and messages outside the function so every call shares them, and count users to know when to disconnect. Simple and dependency-free.
  2. Pinia store. Create the client in a store action, keep status and messages as store state, and let components read from the store. This plays well with Vue Devtools and server-side rendering guards.

Here is a compact version of the singleton approach, saved as src/composables/useSharedMqtt.js:

import { ref, reactive, readonly, onBeforeUnmount } from "vue";
import mqtt from "mqtt";

const status = ref("idle");
const messages = reactive({});
const topicUsers = new Map();
let client = null;
let users = 0;

function ensureClient(url, options) {
  if (client) return;
  client = mqtt.connect(url, { protocolVersion: 5, reconnectPeriod: 3000, ...options });
  client.on("connect", () => {
    status.value = "connected";
    const filters = [...topicUsers.keys()];
    if (filters.length) client.subscribe(filters);
  });
  client.on("reconnect", () => (status.value = "reconnecting"));
  client.on("offline", () => (status.value = "offline"));
  client.on("message", (topic, payload) => {
    let value = payload.toString();
    try { value = JSON.parse(value); } catch { /* keep text */ }
    messages[topic] = { value, receivedAt: Date.now() };
  });
}

export function useSharedMqtt(url, options, topicFilter) {
  ensureClient(url, options);
  users++;
  const count = topicUsers.get(topicFilter) ?? 0;
  topicUsers.set(topicFilter, count + 1);
  if (count === 0 && client.connected) client.subscribe(topicFilter);

  onBeforeUnmount(() => {
    const left = topicUsers.get(topicFilter) - 1;
    if (left === 0) {
      topicUsers.delete(topicFilter);
      client?.unsubscribe(topicFilter);
    } else {
      topicUsers.set(topicFilter, left);
    }
    if (--users === 0) {
      client.end(true);
      client = null;
      status.value = "disconnected";
    }
  });

  return { status: readonly(status), messages: readonly(messages) };
}

Every widget calls useSharedMqtt with its own topic filter. The first caller opens the connection, each topic is subscribed once no matter how many widgets show it, and the last widget to unmount closes the socket. Because messages is a single reactive object keyed by topic, a widget's computed properties only re-evaluate when the specific key they read changes, which keeps large dashboards responsive.

Either way, each browser tab ends up with one MQTT connection, which keeps you well inside connection limits and makes the broker's view of clients easier to understand.

Vue MQTT Performance Tips

A few habits keep a Vue MQTT dashboard smooth as it grows from one widget to dozens:

  • Store the latest value, derive the rest. Keep raw messages in the composable and compute display values such as rounded numbers, units and colours with computed. Computed properties are cached, so they only re-run when the message they depend on changes.
  • Cap chart history. If a widget keeps a rolling series for a sparkline, push new points into a fixed-length array and drop the oldest. Unbounded arrays are the most common cause of dashboards that slow down after a few hours.
  • Use retained messages for first paint. When devices publish state with the retain flag, a widget mounted after navigation shows the current value immediately instead of an empty placeholder.
  • Avoid deep watchers on messages. A watch(messages, ..., { deep: true }) fires on every update to every topic. Watch the single key you care about instead.
  • Show staleness. A value that has not changed for minutes may mean the device is offline. Displaying the update time, as the widget does, prevents people from trusting frozen numbers.

Common Vue MQTT Pitfalls

  1. The client is wrapped in ref(). Symptoms include sluggish performance or odd errors from inside MQTT.js. Keep it in a plain variable or shallowRef.
  2. Connect and disconnect loop. Two components or tabs share a fixed client ID. Generate unique IDs per connection.
  3. Mixed content errors. The page is served over HTTPS but the URL uses ws://. Switch to wss:// on port 8084.
  4. Nothing updates after a network blip. Subscriptions were made outside the connect handler with a clean session.
  5. SSR crashes. In Nuxt or another SSR setup, connect only on the client, which onMounted already guarantees.

Production Setup

For a real application, connect to a private broker with credentials. On ConnectMQ, create a broker in the portal, then pass the generated username and password through Vite environment variables:

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"]
);

Remember that anything bundled into frontend code can be read by users. Use a dedicated dashboard credential, and send sensitive commands through a backend API rather than directly from the browser. A hosted MQTT broker with a valid Let's Encrypt certificate on the WebSocket port means browsers connect without warnings or extra configuration.

If your team also uses other frameworks, compare this composable with the Angular MQTT service built on RxJS. To add compile-time checks for topics and payloads, see type-safe MQTT in TypeScript. The MQTT client libraries guide covers non-JavaScript options for your backend services, and the Vue composables documentation explains the pattern used here in depth.

Get Started with ConnectMQ

Give your Vue app a private broker with secure WebSocket in a few minutes. Sign up at app.connectmq.com, choose the free Serverless plan with no credit card, create a broker and plug the credentials into useMqtt. ConnectMQ's live topic explorer in the portal shows exactly which messages your widgets should be receiving.

Frequently Asked Questions

Which MQTT library should I use with Vue?

MQTT.js is the standard choice. It works in the browser over WebSocket, supports MQTT 3.1.1 and 5.0, and wraps neatly in a composable like the one in this article.

Does this work with Vue 2?

The composable uses the Composition API, which is built into Vue 2.7. For older Vue 2 versions, put the same logic in a component's mounted and beforeDestroy hooks.

Why does my widget show "reconnecting" constantly?

Usually another connection is using the same client ID, a firewall blocks port 8084, or the URL is missing the /mqtt path. Check the browser console for the MQTT.js error message.

Can I publish from a Vue app?

Yes, with the publish function returned by the composable. For actions that affect real equipment, prefer routing them through an authenticated backend so browser users cannot publish arbitrary messages.

Related articles

Best MQTT CLI Tools for Testing, Scripting and Debugging

A hands-on comparison of mosquitto_pub/sub, MQTTX CLI, HiveMQ MQTT CLI, the MQTT.js CLI, curl and mqttui, with the same test run against broker.connectmq.com so you can choose the right tool for scripting and debugging.

8 min read

All articles →

Your private MQTT broker is a few clicks away

Create an account, get your broker credentials and connect your first device — no servers to install, patch or scale.