Each example app in this repository connects to a serial port, but when to disconnect, when to dispose, and where to unsubscribe depends on your framework's component lifecycle. Getting this wrong leaves read pumps or UI subscriptions running after navigation — a common source of resource leaks and confusing reconnect behaviour.
Parent: #585 · Issue: #593 · Related: Quick Start – Disconnect / Dispose · Running imperative methods (cold Observables) · API concepts – SerialSession · Timeout / cancel / retry – Component teardown · Examples
Both methods are cold Observables — they run only when subscribed. See Quick Start – Running imperative methods (cold Observables).
| Method | Port / read pump | Session reuse | All observables | Typical trigger |
|---|---|---|---|---|
disconnect$() |
Closed and stopped | Yes — returns to idle |
Stay active | User clicks Disconnect; reconnect after error; temporary close |
dispose$() |
Closed and stopped | No — terminal | Completed | Component / page teardown; baud-rate change (new session); app shutdown |
For the formal API definitions, see API concepts – SerialSession (disconnect$ / dispose$ sections).
SerialSession instance.idle or error on the same session.// User action — keep the session for a later Connect
session.disconnect$().subscribe({
error: (e) => console.error('Disconnect error:', e),
});
createSerialSession()).// Terminal teardown — create a new session if you need serial again
session.dispose$().subscribe({
error: (e) => console.error('Dispose error:', e),
});
After dispose$, do not call connect$() on the same instance. Create a new createSerialSession().
Apply this order in every framework's cleanup hook:
state$, lines$, terminalText$, and errors$ to your component.disconnect$() is optional before dispose — dispose$() already closes an active connection and stops the read pump.dispose$().subscribe() in the cleanup hook when the session owner is destroyed.// Generic cleanup sketch (adapt to your framework hook)
function teardownSession(session: SerialSession, uiSub: Subscription): void {
uiSub.unsubscribe();
session.dispose$().subscribe({ error: () => void 0 });
}
One-shot subscriptions to connect$(), send$(), or disconnect$() from button handlers usually complete on their own and do not need explicit teardown. Long-lived subscriptions to hot streams (state$, lines$, …) do.
Timeout / cancel / retry – Cancel on Component / Hook teardown shows takeUntil(destroy$) and takeUntilDestroyed() to stop delivering events to your UI.
That is not a substitute for dispose$():
| Action | What it stops | Session port / pump |
|---|---|---|
unsubscribe() / takeUntil |
UI callbacks | Still running if connected |
disconnect$() |
Port and read pump | Session reusable |
dispose$() |
Port, pump, and all session observables | Session terminal |
Always call dispose$() when the session owner is destroyed, even if you already unsubscribed from streams.
The example apps wrap createSerialSession() with a local createSerialSessionController helper (@gurezo/examples-shared, not published on npm). The patterns below apply whether you use that helper or hold a SerialSession directly — call session.dispose$().subscribe() instead of controller.dispose() when you do not use the helper.
Cleanup hook: ngOnDestroy on the service or component that owns the session.
Reference: apps/example-angular/src/app/services/serial-client.service.ts
import { Injectable, OnDestroy } from '@angular/core';
import { createSerialSession, type SerialSession } from '@gurezo/web-serial-rxjs';
@Injectable({ providedIn: 'root' })
export class SerialClientService implements OnDestroy {
private readonly session: SerialSession = createSerialSession({ baudRate: 9600 });
readonly state$ = this.session.state$;
readonly lines$ = this.session.lines$;
readonly errors$ = this.session.errors$;
connect$() {
return this.session.connect$();
}
disconnect$() {
return this.session.disconnect$();
}
ngOnDestroy(): void {
this.session.dispose$().subscribe({ error: () => void 0 });
}
}
Disconnect is triggered from the component on user action:
handleDisconnect(): void {
this.serialService.disconnect$().subscribe({
error: (error: unknown) => console.error('Disconnect error:', error),
});
}
Notes:
providedIn: 'root', ngOnDestroy runs when the Angular application is torn down (for example in tests), not when a single routed component unmounts. For per-route serial access, provide the service at the component level (providers: [SerialClientService]) so ngOnDestroy runs when that component is destroyed.toSignal() or takeUntilDestroyed() for stream subscriptions in components — see Timeout / cancel / retry – Angular.Live example: Angular example app
Cleanup hook: useEffect return function.
Reference: apps/example-react/src/hooks/useSerialSession.ts
import { useEffect, useRef } from 'react';
import { Subscription } from 'rxjs';
import { createSerialSession, type SerialSession } from '@gurezo/web-serial-rxjs';
function useSerialSession() {
const sessionRef = useRef<SerialSession | null>(null);
if (sessionRef.current === null) {
sessionRef.current = createSerialSession({ baudRate: 9600 });
}
useEffect(() => {
const session = sessionRef.current as SerialSession;
const sub = new Subscription();
sub.add(session.state$.subscribe(/* setState */));
sub.add(session.lines$.subscribe(/* onLine */));
sub.add(session.errors$.subscribe(/* onError */));
return () => {
sub.unsubscribe();
session.dispose$().subscribe({ error: () => void 0 });
sessionRef.current = null;
};
}, []);
const disconnect = () =>
(sessionRef.current as SerialSession).disconnect$().subscribe({
error: (e) => console.error('Disconnect error:', e),
});
return { disconnect /* … */ };
}
Notes:
useRef so React StrictMode's double mount/unmount cycle does not lose the instance between effect setup and cleanup.Subscription before calling dispose$().connect$() / disconnect$() / send$() from event handlers are one-shot .subscribe() calls — no extra cleanup needed.Live example: React example app
Cleanup hook: onUnmounted in the composable that owns the session.
Reference: apps/example-vue/src/composables/useSerialClient.ts
import { onUnmounted, ref } from 'vue';
import { createSerialSession, type SerialSession } from '@gurezo/web-serial-rxjs';
export function useSerialClient() {
const session: SerialSession = createSerialSession({ baudRate: 9600 });
const isConnected = ref(false);
const stateSub = session.state$.subscribe((state) => {
isConnected.value = state.status === 'connected';
});
const linesSub = session.lines$.subscribe(/* append to buffer */);
const errorsSub = session.errors$.subscribe(/* show error */);
const disconnect = () =>
session.disconnect$().subscribe({
error: (error: unknown) => console.error('Disconnect error:', error),
});
onUnmounted(() => {
stateSub.unsubscribe();
linesSub.unsubscribe();
errorsSub.unsubscribe();
session.dispose$().subscribe({ error: () => void 0 });
});
return { isConnected, disconnect /* … */ };
}
Notes:
onUnmounted, then dispose the session.Live example: Vue example app
Cleanup hook: onDestroy.
Reference: apps/example-svelte/src/stores/useSerialSession.ts
import { onDestroy } from 'svelte';
import { readable } from 'svelte/store';
import { Subscription } from 'rxjs';
import { createSerialSession, type SerialSession } from '@gurezo/web-serial-rxjs';
export function useSerialSession() {
const session: SerialSession = createSerialSession({ baudRate: 9600 });
// readable store — stop function unsubscribes when last subscriber leaves
const state = readable({ status: 'idle' }, (set) => {
const sub = session.state$.subscribe(set);
return () => sub.unsubscribe();
});
const linesSub = session.lines$.subscribe(/* update store */);
const errorSub = new Subscription();
errorSub.add(session.errors$.subscribe(/* show error */));
onDestroy(() => {
linesSub.unsubscribe();
errorSub.unsubscribe();
session.dispose$().subscribe({ error: () => void 0 });
});
const disconnect = () =>
session.disconnect$().subscribe({
error: (e) => console.error('Disconnect error:', e),
});
return { state, disconnect /* … */ };
}
Notes:
readable stores can auto-unsubscribe via their stop function, but long-lived direct subscriptions (for example terminalText$) still need explicit cleanup in onDestroy.dispose$() in onDestroy when the component that invoked the store factory is destroyed.Live example: Svelte example app
Cleanup hook: explicit destroy() on your app class (or router teardown callback).
Reference: apps/example-vanilla-ts/src/app.ts — the shipped example keeps subscriptions for the app lifetime; production apps should add explicit teardown.
import { createSerialSession, type SerialSession } from '@gurezo/web-serial-rxjs';
import { Subscription } from 'rxjs';
export class App {
private readonly session: SerialSession = createSerialSession({ baudRate: 9600 });
private readonly sub = new Subscription();
constructor() {
this.sub.add(this.session.state$.subscribe(/* update UI */));
this.sub.add(this.session.lines$.subscribe(/* append output */));
this.sub.add(this.session.errors$.subscribe(/* show error */));
disconnectBtn.addEventListener('click', () => {
this.session.disconnect$().subscribe({ error: () => void 0 });
});
window.addEventListener('beforeunload', () => this.destroy());
}
destroy(): void {
this.sub.unsubscribe();
this.session.dispose$().subscribe({ error: () => void 0 });
}
}
Notes:
destroy() yourself — on beforeunload, SPA route change, or when removing the UI.disconnect$(); add destroy() for full session teardown.Live example: Vanilla TS example app
| Question | Answer |
|---|---|
| User clicks Disconnect? | disconnect$().subscribe() — session stays reusable |
| Component / route unmounts? | Unsubscribe UI streams, then dispose$().subscribe() |
| Baud rate changes (new session)? | dispose$() on old session, then createSerialSession() |
After dispose$? |
Create a new session — do not reconnect the old one |
takeUntil(destroy$) enough? |
No — also call dispose$() on the session owner teardown |
| Framework | URL |
|---|---|
| Angular | examples/angular/ |
| React | examples/react/ |
| Vue | examples/vue/ |
| Svelte | examples/svelte/ |
| Vanilla TS | examples/vanilla-ts/ |
For reconnect and error recovery after disconnect (not dispose), see Troubleshooting – Reconnect fails, Troubleshooting – Error Recovery Matrix, and Advanced Usage – Reconnect On Fatal Error.