Il existe deux bonnes façons d'utiliser MQTT dans Angular. ngx-mqtt enveloppe MQTT.js dans un module Angular et fournit MqttService.observe(), qui renvoie des observables RxJS : le moyen le plus rapide d'amener des messages dans un composant. Ou bien vous écrivez vous-même un petit service injectable autour de MQTT.js : une soixantaine de lignes qui vous donnent un contrôle total sur la version du client, les options MQTT 5 et l'état de connexion. Les deux passent par des WebSockets sécurisés (wss://), car les navigateurs ne peuvent pas ouvrir de connexions TCP brutes.
Ce tutoriel présente les deux, branche le résultat dans un composant standalone avec des signals et le teste avec un vrai broker.
Paramètres de connexion
| Paramètre | Valeur |
|---|---|
| URL | wss://broker.connectmq.com:8084/mqtt |
| Hôte / port / chemin | broker.connectmq.com / 8084 / /mqtt |
| Protocole | MQTT 3.1.1 ou 5 |
| Identifiants | un compte navigateur limité aux topics de l'UI |
Une app Angular servie en HTTPS doit utiliser wss:// ; ws:// est bloqué comme contenu mixte. MQTT sur WebSockets détaille le transport.
Option 1 : ngx-mqtt
npm i ngx-mqttngx-mqtt est maintenu indépendamment d'Angular et fige sa propre dépendance MQTT.js. Avant de l'installer, vérifiez que ses peer dependencies correspondent à votre version d'Angular ; c'est la cause la plus fréquente d'erreurs d'installation.
Enregistrez-le une fois dans la configuration d'une app standalone :
import { ApplicationConfig, importProvidersFrom } from '@angular/core';
import { IMqttServiceOptions, MqttModule } from 'ngx-mqtt';
const MQTT_OPTIONS: IMqttServiceOptions = {
hostname: 'broker.connectmq.com',
port: 8084,
protocol: 'wss',
path: '/mqtt',
username: 'username',
password: 'password',
};
export const appConfig: ApplicationConfig = {
providers: [importProvidersFrom(MqttModule.forRoot(MQTT_OPTIONS))],
};Injectez ensuite MqttService là où vous avez besoin des messages :
import { Component, inject } from '@angular/core';
import { toSignal } from '@angular/core/rxjs-interop';
import { IMqttMessage, MqttService } from 'ngx-mqtt';
import { map } from 'rxjs';
@Component({
selector: 'app-status',
template: `<p>Last status: {{ status() }}</p>
<button (click)="ping()">Ping</button>`,
})
export class StatusComponent {
private readonly mqtt = inject(MqttService);
readonly status = toSignal(
this.mqtt.observe('testmqtt/angular/status').pipe(map((m: IMqttMessage) => m.payload.toString())),
{ initialValue: '—' },
);
ping() {
this.mqtt.unsafePublish('testmqtt/angular/cmd/ping', String(Date.now()), { qos: 1 });
}
}observe() partage un seul abonnement broker entre tous les observateurs du même filtre et le résilie quand le dernier s'en va. publish() renvoie un observable qui se termine quand le broker accuse réception du message ; unsafePublish() envoie sans attendre.
Option 2 : votre propre service MQTT.js
Si vous voulez la version actuelle de MQTT.js, les propriétés MQTT 5 ou simplement moins de dépendances, un service écrit à la main est simple :
npm i mqttimport { Injectable, OnDestroy, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
import mqtt, { type MqttClient } from 'mqtt';
import { BehaviorSubject, Observable, Subject, filter, map } from 'rxjs';
import { environment } from '../environments/environment';
export type MqttState = 'idle' | 'connecting' | 'connected' | 'reconnecting' | 'offline' | 'error';
export interface MqttMessage<T> { topic: string; data: T }
export function matches(topicFilter: string, topic: string): boolean {
const f = topicFilter.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;
}
@Injectable({ providedIn: 'root' })
export class MqttClientService implements OnDestroy {
readonly state$ = new BehaviorSubject<MqttState>('idle');
private readonly incoming$ = new Subject<{ topic: string; payload: Uint8Array }>();
private readonly refCounts = new Map<string, number>();
private client: MqttClient | null = null;
constructor() {
if (!isPlatformBrowser(inject(PLATFORM_ID))) return; // pas de MQTT pendant le SSR
this.state$.next('connecting');
this.client = mqtt.connect(environment.mqtt.url, {
username: environment.mqtt.username,
password: environment.mqtt.password,
clientId: `ng-${crypto.randomUUID().slice(0, 8)}`,
reconnectPeriod: 2000,
connectTimeout: 10_000,
});
this.client.on('connect', () => this.state$.next('connected'));
this.client.on('reconnect', () => this.state$.next('reconnecting'));
this.client.on('offline', () => this.state$.next('offline'));
this.client.on('error', () => this.state$.next('error'));
this.client.on('message', (topic, payload) => this.incoming$.next({ topic, payload }));
}
observe<T = unknown>(topicFilter: string): Observable<MqttMessage<T>> {
return new Observable<MqttMessage<T>>((subscriber) => {
this.retain(topicFilter);
const sub = this.incoming$
.pipe(
filter((m) => matches(topicFilter, m.topic)),
map((m) => {
const text = new TextDecoder().decode(m.payload);
let data: T;
try { data = JSON.parse(text) as T; } catch { data = text as T; }
return { topic: m.topic, data };
}),
)
.subscribe(subscriber);
return () => {
sub.unsubscribe();
this.release(topicFilter);
};
});
}
async publish(topic: string, message: string, retain = false): Promise<void> {
await this.client?.publishAsync(topic, message, { qos: 1, retain });
}
ngOnDestroy(): void {
this.client?.end(true);
}
private retain(topicFilter: string) {
const n = this.refCounts.get(topicFilter) ?? 0;
if (n === 0) this.client?.subscribe(topicFilter, { qos: 1 });
this.refCounts.set(topicFilter, n + 1);
}
private release(topicFilter: string) {
const n = (this.refCounts.get(topicFilter) ?? 1) - 1;
if (n === 0) {
this.refCounts.delete(topicFilter);
this.client?.unsubscribe(topicFilter);
} else {
this.refCounts.set(topicFilter, n);
}
}
}Les choix de conception :
- Un client par app.
providedIn: 'root'fait du service un singleton : tous les composants partagent un seul WebSocket. - Des observables froids à comptage de références. Rien n'est abonné sur le broker tant qu'aucun composant ne s'abonne, et le topic est désabonné quand le dernier composant se désabonne. MQTT.js se réabonne seul aux topics actifs après une reconnexion.
- Compatible SSR. Côté serveur, le service reste
idle: Angular SSR ou le prerendering n'ouvrent jamais de socket. - Les wildcards sont résolus côté client. Le broker envoie chaque message une seule fois même si plusieurs filtres correspondent ;
matches()le route vers les bons observateurs. Testez vos filtres dans le topic matcher.
Un composant avec des signals
import { Component, inject } from '@angular/core';
import { DecimalPipe } from '@angular/common';
import { toSignal } from '@angular/core/rxjs-interop';
import { auditTime, map, scan } from 'rxjs';
import { MqttClientService } from './mqtt-client.service';
interface Reading { t: number; h?: number }
@Component({
selector: 'app-sensors',
imports: [DecimalPipe],
template: `
<p>Broker: {{ state() }}</p>
<table>
@for (row of rows(); track row.device) {
<tr><td>{{ row.device }}</td><td>{{ row.t | number: '1.1-1' }} °C</td></tr>
}
</table>
<button [disabled]="state() !== 'connected'" (click)="ping()">Ping</button>
`,
})
export class SensorsComponent {
private readonly mqtt = inject(MqttClientService);
readonly state = toSignal(this.mqtt.state$, { requireSync: true });
readonly rows = toSignal(
this.mqtt.observe<Reading>('testmqtt/angular/sensors/+').pipe(
scan((acc, m) => ({ ...acc, [m.topic.split('/').pop()!]: m.data.t }), {} as Record<string, number>),
auditTime(100),
map((latest) => Object.entries(latest).map(([device, t]) => ({ device, t }))),
),
{ initialValue: [] },
);
ping() {
this.mqtt.publish('testmqtt/angular/cmd/ping', String(Date.now()));
}
}toSignal() s'abonne dans le contexte d'injection du composant et se désabonne automatiquement à sa destruction : ni ngOnDestroy, ni takeUntil. scan conserve la dernière valeur par appareil, et auditTime(100) limite les mises à jour de l'UI à dix par seconde quelle que soit la cadence des messages, ce qui garde la détection de changements peu coûteuse sur les topics chargés. (Sur les versions d'Angular antérieures à la 19, ajoutez standalone: true au décorateur.)
Si vous préférez les observables dans les templates, observe() fonctionne tout aussi bien avec le pipe async ou avec takeUntilDestroyed() dans un service.
Tests
Vérification manuelle de bout en bout
- Lancez
ng serveet vérifiez que le composant afficheBroker: connected. S'il afficheerror, ouvrez la console du navigateur :Connection refused: Not authorizedsignale les identifiants ou les ACL ; un handshake WebSocket en échec désigne l'URL, le port8084ou le chemin/mqtt. - Ouvrez le client MQTT en ligne, connectez-vous au même broker et publiez
{"t": 23.4}surtestmqtt/angular/sensors/room-1. La ligne apparaît en moins de 100 ms. - Publiez sur
testmqtt/angular/sensors/room-2et vérifiez que les deux lignes s'affichent. - Abonnez-vous à
testmqtt/angular/#dans le client en ligne et cliquez sur Ping pour vérifier la publication. - Quittez le composant et confirmez, dans un debugger ou le log du broker, que le topic est désabonné.
Publier des mesures de test vers votre app Angular
Tests unitaires
Les composants ne devraient pas avoir besoin d'un broker. Remplacez le service par un stub basé sur un Subject :
import { TestBed } from '@angular/core/testing';
import { BehaviorSubject, Subject } from 'rxjs';
import { MqttClientService, MqttMessage } from './mqtt-client.service';
import { SensorsComponent } from './sensors.component';
it('renders the latest reading per device', async () => {
const messages = new Subject<MqttMessage<{ t: number }>>();
await TestBed.configureTestingModule({
imports: [SensorsComponent],
providers: [{
provide: MqttClientService,
useValue: { state$: new BehaviorSubject('connected'), observe: () => messages, publish: async () => {} },
}],
}).compileComponents();
const fixture = TestBed.createComponent(SensorsComponent);
fixture.detectChanges();
messages.next({ topic: 'testmqtt/angular/sensors/room-1', data: { t: 21 } });
await new Promise((r) => setTimeout(r, 150)); // attendre auditTime
fixture.detectChanges();
expect(fixture.nativeElement.textContent).toContain('room-1');
});Testez le service lui-même, y compris matches() et le comportement de reconnexion, contre un vrai broker dans une petite suite d'intégration. Le guide des tests avec Jest présente des modèles transposables aux test runners Angular, et MQTT avec TypeScript couvre le typage des payloads de bout en bout.
Dépannage
| Symptôme | Cause | Correction |
|---|---|---|
npm i ngx-mqtt échoue sur un conflit de peer dependencies | Bibliothèque pas encore mise à jour pour votre version d'Angular | Utiliser le service MQTT.js de l'option 2 |
| Le build SSR bloque ou expire | Le client se connecte côté serveur | Protéger avec isPlatformBrowser ou afterNextRender() |
| L'UI rame sur les topics chargés | Détection de changements à chaque message | Limiter avec auditTime ou throttleTime |
| Les onglets se déconnectent mutuellement | Même clientId partout | Générer un client ID aléatoire par onglet |
Pour d'autres messages d'erreur, voir les erreurs MQTT.js.
Angular et MQTT vont naturellement ensemble, car les deux raisonnent en flux : un service singleton possède la connexion, les composants consomment des observables ou des signals, et le nettoyage se fait quand le composant disparaît. Commencez avec ngx-mqtt s'il prend en charge votre version d'Angular, passez au service MQTT.js quand il vous faut plus de contrôle, et vérifiez chaque topic avec un second client avant de livrer.
Questions fréquentes
Faut-il utiliser ngx-mqtt ou MQTT.js directement dans Angular ?
ngx-mqtt est le démarrage le plus rapide : un appel à forRoot() et une méthode observe() qui renvoie des observables RxJS. Un petit service autour de MQTT.js vous donne la dernière version de MQTT.js, les options MQTT 5 et un contrôle total, sans dépendre d'un wrapper qui suive le rythme d'Angular.
Dois-je me désabonner des observables MQTT dans les composants Angular ?
Oui, ou utiliser un mécanisme qui le fait pour vous. toSignal(), le pipe async et takeUntilDestroyed() se désabonnent à la destruction du composant. Avec une méthode observe() à comptage de références, le topic est aussi désabonné sur le broker quand plus personne n'écoute.
Pourquoi Angular SSR bloque-t-il ou échoue-t-il quand mon app utilise MQTT ?
Le client MQTT tente d'ouvrir un WebSocket côté serveur et empêche le rendu de se terminer. Ne vous connectez que dans le navigateur : vérifiez isPlatformBrowser(PLATFORM_ID) avant de vous connecter ou démarrez la connexion dans afterNextRender().



