NgRx for Real‑Time Dashboards in Angular 20+: Typed WebSocket Streams, Optimistic Updates, and Telemetry Effects

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: 2

Don’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.

Related Resources

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.

Hire Matthew – Remote Angular Expert, Available Now See how we rescue chaotic code with gitPlumbers (70% velocity boost)

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
NG Wave Component Library

Related resources