
NgRx for Real‑Time Dashboards in Angular 20+: Typed WebSocket Streams, Optimistic Updates, and Telemetry Effects
Enterprise‑grade patterns I use to keep high‑volume dashboards smooth: typed actions, resilient WebSocket effects, optimistic updates with rollbacks, and Signals‑friendly selectors.
Typed streams, one socket effect, and normalized state—do that, and your dashboard stops jittering.Back to all posts
I’ve shipped and stabilized real‑time Angular dashboards for media, aviation, telecom, and logistics. When the stream spikes, bad state patterns surface instantly: jittery cards, duplicate rows, stale aggregates. This piece shows the NgRx approach I use on Angular 20+ to keep telemetry fast, typed, and predictable—even at enterprise volumes.
We’ll wire a typed WebSocket effect, normalize events with EntityAdapter, and bridge selectors into Signals so your PrimeNG/Material dashboards stay smooth. I’ll also show optimistic updates with rollbacks using correlation IDs—what kept ads analytics at a leading telecom provider and kiosk flows at a major airline responsive when networks misbehaved.
When Your Dashboard Jitters: NgRx Patterns for Real‑Time Telemetry in Angular 20+
Real‑time means bursts, partial failures, and evolving schemas. The stack here: Angular 20, NgRx Store/Effects/Entity, Signals/SignalStore, RxJS 7, PrimeNG tables/charts, and Firebase/GA4 for telemetry. Nx keeps the contracts shared across libs and services.
A real scene from production
at a leading telecom provider, ad impressions and pacing updates arrived 1000s/sec during live events. Unbounded merges caused duplicate rows and re-render storms. at a major airline, kiosk device telemetry flapped during Wi‑Fi handoffs. The fix wasn’t a bigger instance—it was typed streams, normalized state, and disciplined effects.
Why me for this job
If you need to hire an Angular developer or Angular consultant to stabilize a real‑time dashboard, this is the playbook I bring to Angular 20+ teams—NgRx + Signals, strong typing, and CI guardrails.
a global entertainment company workforce tracking
United airport kiosk simulation (Docker)
Charter ads analytics
a broadcast media network VPS scheduling
an insurance technology company telematics dashboards
Why Angular Teams Break on WebSockets and Optimistic Updates
Common failure modes I find during rescues
These show up as jitter, dropped updates, or stuck spinners. Fixing them is less about magic operators and more about boundaries: one connection effect, normalized state, typed actions, and deterministic UX paths.
Untyped message parsing and switch/case fall-through
Multiple sockets per page; no backoff/heartbeat
Reducers storing raw arrays; O(n) duplicates
Optimistic updates without correlation/rollback
Selectors that force full-table re-renders
Architecture: NgRx + Signals + WebSocketSubject + EntityAdapter
// libs/telemetry/src/lib/contracts/telemetry-event.ts
export type TelemetryEvent =
| { type: 'metric'; id: string; ts: number; kpi: string; value: number; tenant: string }
| { type: 'status'; id: string; ts: number; deviceId: string; state: 'online'|'offline'|'degraded'; tenant: string }
| { type: 'ack'; correlationId: string; ok: boolean; reason?: string };
// libs/telemetry/src/lib/state/telemetry.actions.ts
import { createActionGroup, props } from '@ngrx/store';
export const TelemetryActions = createActionGroup({
source: 'Telemetry',
events: {
'Connect': props<{ url: string; token: string }>(),
'Connected': props<void>(),
'Disconnected': props<{ reason?: string }>(),
'Incoming Event': props<{ event: TelemetryEvent }>(),
'Send Command': props<{ payload: unknown; correlationId: string }>(),
'Command Ack': props<{ correlationId: string; ok: boolean; reason?: string }>(),
'Error': props<{ error: unknown }>(),
}
});
// libs/telemetry/src/lib/state/telemetry.effects.ts
import { Injectable, inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { TelemetryActions } from './telemetry.actions';
import { webSocket, WebSocketSubject } from 'rxjs/webSocket';
import { EMPTY, interval, merge, of } from 'rxjs';
import { catchError, distinctUntilChanged, filter, map, mergeMap, retry, shareReplay, switchMap, takeUntil, tap } from 'rxjs/operators';
@Injectable({ providedIn: 'root' })
export class TelemetryEffects {
private actions$ = inject(Actions);
private socket?: WebSocketSubject<unknown>;
connect$ = createEffect(() => this.actions$.pipe(
ofType(TelemetryActions.connect),
switchMap(({ url, token }) => {
const ws = webSocket({ url: `${url}?token=${token}`, deserializer: e => JSON.parse(e.data) });
this.socket = ws;
const heartbeats$ = interval(15000).pipe(
tap(() => ws.next({ type: 'ping', ts: Date.now() })),
);
const incoming$ = ws.pipe(
map((event: any) => TelemetryActions.incomingEvent({ event })),
catchError(error => of(TelemetryActions.error({ error })))
);
const stream$ = merge(incoming$, heartbeats$).pipe(
retry({ count: Infinity, delay: (_e, i) => Math.min(1000 * 2 ** i, 30000) }),
takeUntil(this.actions$.pipe(ofType(TelemetryActions.disconnected)))
);
return merge(of(TelemetryActions.connected()), stream$);
})
));
sendCommand$ = createEffect(() => this.actions$.pipe(
ofType(TelemetryActions.sendCommand),
tap(({ payload, correlationId }) => this.socket?.next({ ...payload, correlationId })),
// noop effect: command echoes come via incomingEvent
switchMap(() => EMPTY)
), { dispatch: false });
}Define a typed telemetry contract
Model messages as a discriminated union. I prefer TypeScript plus runtime validation (zod/io-ts) at the boundary so effects can trust types inside reducers.
Single connection effect
One effect owns the socket lifecycle to avoid racing connections and memory leaks.
Exponential backoff with jitter
Heartbeat ping/pong
Auth refresh
Idempotency via sequence numbers
Normalize and select
Keep the UI stable by feeding Signals from selectors—components react surgically without thrashing.
EntityAdapter keyed by composite id
Memoized selectors per partition/tenant
toSignal bridge for components
Reducers, Selectors, and Signal Bridges
// libs/telemetry/src/lib/state/telemetry.reducer.ts
import { createReducer, on, createFeature } from '@ngrx/store';
import { createEntityAdapter, EntityState } from '@ngrx/entity';
import { TelemetryActions } from './telemetry.actions';
import { TelemetryEvent } from '../contracts/telemetry-event';
interface Metric { id: string; ts: number; kpi: string; value: number; tenant: string; }
interface Status { id: string; ts: number; deviceId: string; state: 'online'|'offline'|'degraded'; tenant: string; }
const metricAdapter = createEntityAdapter<Metric>({ selectId: (m) => `${m.tenant}:${m.id}:${m.kpi}` });
const statusAdapter = createEntityAdapter<Status>({ selectId: (s) => `${s.tenant}:${s.deviceId}` });
interface State {
metrics: EntityState<Metric>;
statuses: EntityState<Status>;
connected: boolean;
}
const initialState: State = {
metrics: metricAdapter.getInitialState(),
statuses: statusAdapter.getInitialState(),
connected: false,
};
export const telemetryReducer = createReducer(
initialState,
on(TelemetryActions.connected, (s) => ({ ...s, connected: true })),
on(TelemetryActions.disconnected, (s) => ({ ...s, connected: false })),
on(TelemetryActions.incomingEvent, (s, { event }) => {
switch (event.type) {
case 'metric': return { ...s, metrics: metricAdapter.upsertOne(event, s.metrics) };
case 'status': return { ...s, statuses: statusAdapter.upsertOne(event, s.statuses) };
case 'ack': return s; // handled in optimistic effect
default: return s;
}
})
);
export const telemetryFeature = createFeature({ name: 'telemetry', reducer: telemetryReducer });
// libs/telemetry/src/lib/state/telemetry.selectors.ts
import { createSelector } from '@ngrx/store';
const { selectMetricsState, selectStatusesState } = telemetryFeature;
const metricSelectors = metricAdapter.getSelectors(selectMetricsState);
const statusSelectors = statusAdapter.getSelectors(selectStatusesState);
export const selectTenantMetrics = (tenant: string) => createSelector(
metricSelectors.selectAll,
(all) => all.filter(m => m.tenant === tenant)
);
export const selectDeviceStatus = (tenant: string, deviceId: string) => createSelector(
statusSelectors.selectEntities,
(entities) => entities[`${tenant}:${deviceId}`]
);
// libs/telemetry/src/lib/dashboard/dashboard.component.ts
import { toSignal } from '@angular/core/rxjs-interop';
const tenant = 'acme';
metricsSig = toSignal(this.store.select(selectTenantMetrics(tenant)), { initialValue: [] });
statusSig = toSignal(this.store.select(selectDeviceStatus(tenant, 'kiosk-42')));
latestKpiSig = computed(() => {
const byKpi = new Map<string, number>();
for (const m of this.metricsSig()) byKpi.set(m.kpi, m.value);
return byKpi; // used by charts
});Normalize with EntityAdapter
High-volume streams must be O(1) upserts. Use composable keys that prevent duplicates across tenants/partitions.
Selectors to Signals
This keeps PrimeNG tables/charts from re-rendering entire datasets every tick.
Prefer fine-grained selectors over selector that returns arrays
Use toSignal for reactive components
Computed signals for aggregates
Optimistic Updates with Rollbacks and Correlation IDs
// libs/telemetry/src/lib/state/optimistic.actions.ts
export const OptimisticActions = createActionGroup({
source: 'Optimistic',
events: {
'Start': props<{ id: string; patch: Partial<Status>; correlationId: string }>(),
'Apply Local': props<{ id: string; patch: Partial<Status>; correlationId: string }>(),
'Rollback': props<{ id: string; prev: Status; correlationId: string; reason?: string }>(),
'Commit': props<{ correlationId: string }>(),
}
});
// effect wires Start -> local apply + send command
start$ = createEffect(() => this.actions$.pipe(
ofType(OptimisticActions.start),
tap(({ id, patch, correlationId }) => this.store.dispatch(OptimisticActions.applyLocal({ id, patch, correlationId }))),
map(({ id, patch, correlationId }) => TelemetryActions.sendCommand({ payload: { type: 'setStatus', id, ...patch }, correlationId }))
));
// incoming acks reconcile
ack$ = createEffect(() => this.actions$.pipe(
ofType(TelemetryActions.incomingEvent),
filter(({ event }) => event.type === 'ack'),
map(({ event }) => event as { type: 'ack'; correlationId: string; ok: boolean; reason?: string }),
mergeMap(({ correlationId, ok, reason }) => ok
? of(OptimisticActions.commit({ correlationId }), TelemetryActions.commandAck({ correlationId, ok }))
: of(OptimisticActions.rollback({ id: 'n/a', prev: {} as any, correlationId, reason }), TelemetryActions.commandAck({ correlationId, ok, reason }))
)
));
// reducer keeps a small pending map keyed by correlationId
interface Pending { [correlationId: string]: { id: string; prev: Status } }
on(OptimisticActions.applyLocal, (s, { id, patch, correlationId }) => {
const prev = statusAdapter.getSelectors().selectEntities(s.statuses)[id]!;
return {
...s,
statuses: statusAdapter.updateOne({ id, changes: patch }, s.statuses),
pending: { ...s.pending, [correlationId]: { id, prev } }
};
}),
on(OptimisticActions.rollback, (s, { correlationId }) => {
const p = s.pending[correlationId];
if (!p) return s;
const { id, prev } = p;
const next = statusAdapter.upsertOne(prev, s.statuses);
const { [correlationId]: _, ...rest } = s.pending;
return { ...s, statuses: next, pending: rest };
}),
on(OptimisticActions.commit, (s, { correlationId }) => {
const { [correlationId]: _, ...rest } = s.pending;
return { ...s, pending: rest };
})Rule of thumb
Use a dedicated slice for pending commands so the UI can explain what’s in flight and why.
Always emit a local change tagged with correlationId
Render immediately for UX
Reconcile on ack/nack without flicker
Effect and reducer
This is the pattern I used to keep ad pacing controls snappy and kiosk mode changes instant—user sees success immediately, and we reconcile when the server replies.
Production Examples: Ads Analytics and Kiosk Telemetry
Charter ads analytics
We normalized events by campaign/placement and rendered pace deltas via computed signals. Angular DevTools showed steady render counts even at peak.
Spiky traffic during live events
Typed schemas and partitioned selectors
<100ms median UI latency
United kiosk telemetry
We simulated readers/printers/scanners in Docker, used optimistic mode switches (online/offline/maintenance), and reconciled with device acks when the network returned.
Dockerized hardware simulation
Offline‑tolerant flows and retries
Peripheral state reflected instantly
Dashboard UX: Backpressure, Virtualization, and PrimeNG
# tools/ci/render-guard.yml (conceptual)
checks:
dashboard-home:
maxRenders: 3 # per signal change
campaign-table:
maxRenders: 2Don’t flood the DOM
PrimeNG data tables + virtualization cut reflow cost massively. For charts (Highcharts/D3), update series in micro-batches keyed off computed signals to avoid churn.
Windowed lists (cdk-virtual-scroll-viewport)
Batch updates with animationFrameScheduler
Coalesce chart updates
Backpressure at the effect boundary
A simple auditTime(16) before dispatching metric bursts is often enough to keep 60fps without losing user-visible fidelity.
Buffer with time or count
Drop stale snapshots, keep latest
Apply flow control before reducers
Measure and prove
I track re-render counts per route and push them into CI as a guardrail. If a PR doubles renders, it fails. This is how we kept dashboards smooth at a broadcast media network and an insurance technology company.
Angular DevTools flame charts
Render count budgets in CI
GA4/Firebase custom events
When to Hire an Angular Developer for Legacy Rescue (WebSocket State)
Signs you need help now
If this sounds familiar, bring in a senior Angular engineer. I’ll audit your NgRx slices, effects, and Signals integration, then land fixes behind feature flags so you don’t break prod while stabilizing.
Multiple sockets per component/page
Selectors returning new arrays on every tick
Optimistic updates without rollback path
Leaky effects and memory spikes
SSR/SEO broken by non-deterministic streams
Typical timeline
As a remote Angular consultant, I can start with a code review and a typed event contract. We’ll set measurable goals: <100ms median update latency, stable render counts, zero duplicate rows.
Assessment in 1 week
Stabilization in 2–4 weeks
Full modernization 4–8 weeks
How an Angular Consultant Structures Typed Actions and Effects for Telemetry
Playbook I run on AngularUX projects
This is the same approach behind my live products—gitPlumbers (99.98% uptime) and IntegrityLens (12k+ interviews)—and what I’ll apply to your telemetry dashboard.
Contracts first: shared types/lib in Nx
One socket effect, lifecycle actions
Entity normalization + strict selectors
Signals bridge + virtualization
Optimistic updates with correlation IDs
CI guardrails: schema checks, render budgets
Key Takeaways and What to Instrument Next
Summing it up
If you’re planning Q1 2025 roadmaps, instrument first, then refactor. Your execs will see the before/after in Lighthouse and GA4, and your engineers will see it in flame charts.
Typed unions + action groups keep effects honest.
One WebSocket effect with backoff and heartbeats.
EntityAdapter + Signals prevent UI jitter.
Optimistic updates must have deterministic rollback.
Measure render counts and latency in CI.
Key takeaways
- Model telemetry with a discriminated union and action groups to keep effects fully typed.
- Use a single WebSocket effect with exponential backoff, heartbeats, and idempotency guards.
- Normalize high-volume streams via EntityAdapter; bridge selectors to Signals for zero-jitter UI.
- Do optimistic updates with correlation IDs and deterministic rollbacks on server NACK.
- Instrument everything: render counts, flame charts, GA4/Firebase events, and effect-level logs.
Implementation checklist
- Define a typed TelemetryEvent union and schema validators.
- Create action groups for connection lifecycle, incoming messages, and commands.
- Implement a resilient WebSocket effect: backoff, heartbeats, queueing, and re-auth.
- Normalize events with NgRx EntityAdapter; write memoized selectors.
- Bridge selectors to Signals via toSignal; measure render counts with Angular DevTools.
- Implement optimistic updates with correlation IDs and rollback actions.
- Add backpressure and virtualization for large tables/feeds.
- Guard coverage in CI: action exhaustiveness, effect tests, contract tests against mock server.
Questions we hear from teams
- How long does a real-time Angular dashboard stabilization take?
- Typical engagements: 2–4 weeks for rescues, 4–8 weeks for full upgrades. First week covers typed contracts, effects audit, and render metrics. Then we land normalized state, Signals bridges, and optimistic updates behind flags to avoid production risk.
- Do I need Signals if I already use NgRx?
- Yes for components. Keep NgRx for global state and effects; use Signals via toSignal and computed for jitter-free UI. This hybrid pattern is battle-tested on Angular 20+ with PrimeNG tables and real-time charts.
- How do you handle WebSocket disconnects and retries?
- One effect owns the socket with exponential backoff, heartbeats, and auth refresh. We add idempotency checks and sequence numbers to prevent duplicates and reconcile on reconnects—no double counting in reducers.
- What does an Angular consultant deliver in week one?
- A typed event schema, connection lifecycle actions, a draft WebSocket effect, and a normalization plan (EntityAdapter + selectors). You’ll also get a render-count baseline and CI guardrails for regression detection.
- How much does it cost to hire an Angular developer for this work?
- It depends on scope and compliance requirements. Most teams start with a fixed-price assessment, then a short stabilization sprint. Book a discovery call—I’ll share a transparent estimate and milestones tied to measurable UX metrics.
Ready to level up your Angular experience?
Let AngularUX review your Signals roadmap, design system, or SSR deployment plan.
NG Wave
Angular Component Library
A comprehensive collection of 110+ animated, interactive, and customizable Angular components. Converted from React Bits with full feature parity, built with Angular Signals, GSAP animations, and Three.js for stunning visual effects.
Explore Components