Live Examples – Modern Angular Patterns (2025): Signals, NgRx, RxJS, Web Components, A11y & Performance Testing

Live demo: StackBlitz · Source: GitHub

I pulled together a hands-on Angular modern patterns showcase that demonstrates how to combine Signals, NgRx, RxJS, Web Components, and a pragmatic performance + a11y playbook. It targets the latest Angular (standalone components & new control flow) and is meant to be read in the code—then run, tweak, and profile.

Why this exists

Signals unlocked fine‑grained reactivity, standalone components simplified architecture, and NgRx matured into an ergonomic state stack. You’ll find a lot of articles about each thing in isolation; this repo focuses on how they fit together in real UI flows.

Quick start

Run it locally

git clone https://github.com/jdavis-software/angular-modern-patterns-showcase
cd angular-modern-patterns-showcase
npm i
npm start
# Visit http://localhost:4200

Or open in your browser (no setup):

👉 StackBlitz live demo

Contents at a glance

  • Signals vs NgRx — clear division of responsibility
  • When RxJS still shines — external streams, back-pressure, and composition
  • Web Components in Angular — native + Angular Elements, Shadow DOM styling
  • Performance playbook — render less, compute once, stream smart
  • Accessibility (WCAG 2.2) — keyboard-first and screen reader friendly
  • Performance testing — devtools, lab checks, and CI ideas

Tip: Scan the component folders and open the matching route in the running app to see each pattern live.

1) Signals: feature‑local state & derived data

Use Signals for UI-centric or feature-local state. Compute derivations and memoize expensive work with computed. Keep it close to the component.

import { signal, computed, effect } from '@angular/core';

type CartItem = { name: string; price: number; qty: number };

export class SignalsCartComponent {
  readonly items = signal<CartItem[]>([]);

  readonly count = computed(() => this.items().reduce((n, i) => n + i.qty, 0));
  readonly total = computed(() => this.items().reduce((s, i) => s + i.price * i.qty, 0));

  constructor() {
    effect(() => console.log('cart changed', this.items()));
  }

  add(item: CartItem) { this.items.update(xs => [...xs, item]); }
  clear() { this.items.set([]); }
}

Heuristics

  • If state is owned by a single feature/view and derived mostly for rendering, keep it in Signals.
  • Prefer computed over ad‑hoc transforms in templates to remove work from change detection.
  • Use small, testable derivations instead of big monolithic selectors where only one view cares.

2) NgRx: app‑wide state, effects, and time‑travel

Use NgRx when multiple features need the same source of truth, you need effects orchestration (server calls, caching, retries), or time‑travel/debuggability matters.

Bridge NgRx selectors to Signals with toSignal for ergonomic templates:

import { Store } from '@ngrx/store';
import { toSignal } from '@angular/core/rxjs-interop';
import { selectVisibleTodos } from './store/selectors';
import { todoToggled } from './store/actions';

export class TodosComponent {
  readonly todosSig = toSignal(this.store.select(selectVisibleTodos), { initialValue: [] });

  constructor(private store: Store) {}

  toggle(id: string) {
    this.store.dispatch(todoToggled({ id }));
  }
}

Rule of thumb

  • Signals: local view state + derived render data
  • NgRx: cross‑feature domain state + effects + devtools

3) Where RxJS still shines

Even with Signals, RxJS is unrivaled for external/continuous streams (websockets, DOM events, SSE), back‑pressure, multicasting, and complex async composition. Expose the result to the view as a Signal.

import { interval, Subject } from 'rxjs';
import { bufferTime, map, shareReplay } from 'rxjs/operators';

const source$ = interval(100); // noisy stream
export const batches$ = source$.pipe(
  bufferTime(2000),
  map(batch => ({ count: batch.length, last: batch.at(-1) })),
  shareReplay({ bufferSize: 1, refCount: true }),
);
import { toSignal } from '@angular/core/rxjs-interop';
export class DashboardComponent {
  readonly batchSig = toSignal(batches$, { initialValue: { count: 0, last: null } });
}

Guideline

  • Keep RxJS at the boundaries and for non-trivial async flows; bridge to Signals for consumption in templates.

4) Web Components in Angular

The repo shows two interop paths:

A) Consume native custom elements

  • Bind attributes/props like normal.
  • Listen to well-typed CustomEvent contracts—treat events as part of your API.
  • Theme via CSS custom properties that pierce Shadow DOM.
<progress-ring value="{{percent()}}" aria-label="Upload progress"></progress-ring>

B) Export Angular as a custom element (Angular Elements)

import { Injector } from '@angular/core';
import { createCustomElement } from '@angular/elements';
import { ProgressRingComponent } from './progress-ring.component';

export class AppModule {
  constructor(injector: Injector) {
    const ProgressEl = createCustomElement(ProgressRingComponent, { injector });
    customElements.define('progress-ring', ProgressEl);
  }
}

Styling across Shadow DOM

  • Prefer design tokens via CSS variables (e.g., --ring-color).
  • Avoid deep selectors; keep styling opt‑in and documented in the component README.

5) Performance playbook (practical)

  • Render less: use trackBy on every *ngFor; avoid recomputing arrays/objects in templates.
  • Compute once: push expensive work into computed signals or memoized selectors.
  • New control flow & @defer: lazy‑render non‑critical UI.
  • Change detection: drive updates via signals to minimize checks.
  • Virtualization: use CDK virtual scroll for large lists.
  • Network: preconnect critical origins; cache smartly at the HTTP layer.
@defer (on viewport) {
  <heavy-widget></heavy-widget>
}
<li *ngFor="let user of users(); trackBy: userId">{{ user.name }}</li>
userId = (_: number, u: { id: string }) => u.id;

6) Accessibility (WCAG 2.2 minded)

  • Semantic HTML first; ARIA only to fill gaps.
  • Keyboard-first: visible focus, logical tab order, and roving tabindex for composite widgets.
  • Focus trapping for dialogs; Escape closes; return focus to the invoker.
  • Live regions for async updates (e.g., background save).
// Roving tabindex (essentials)
current = signal(0);
onKey(e: KeyboardEvent) {
  if (e.key === 'ArrowRight') this.current.update(i => (i + 1) % this.items.length);
  if (e.key === 'ArrowLeft')  this.current.update(i => (i - 1 + this.items.length) % this.items.length);
}

7) Performance testing: how to measure this repo

You don’t improve what you don’t measure. Here are low‑friction ways to profile the examples locally or on StackBlitz.

A) Angular DevTools Profiler (Chrome)

  1. Open the app, install Angular DevTools.
  2. Navigate to a demo (e.g., list rendering).
  3. Start profiling, interact (scroll/filter), stop and inspect change detection cycles and component hotspots.

B) Chrome Performance panel

  1. npm start, open http://localhost:4200.
  2. DevTools → Performance → Record.
  3. Interact with the demo; Stop.
  4. Inspect Main thread (scripting/layout/paint), long tasks, and FPS.

C) Lighthouse (lab checks)

With the app running locally:

npx lighthouse http://localhost:4200 --only-categories=performance --view

Compare before/after toggling patterns (e.g., adding trackBy, enabling @defer).

D) Web Vitals in code (field-ish signals)

Add a basic Web Vitals logger (optional) to see CLS/LCP/INP in DevTools console in dev builds.

// vitals.ts (add where convenient in dev)
import { onCLS, onLCP, onINP } from 'web-vitals';
onCLS(console.log);
onLCP(console.log);
onINP(console.log);

For CI: consider scripting a headless Lighthouse run against a static build and diffing scores to prevent performance regressions.

How to use these patterns at work

  • Start with Signals for local, view-level state. Promote to NgRx only when multiple features require the same data, or effects orchestration/debuggability are needed.
  • Keep RxJS at the edges and for non-trivial async flows, then bridge to Signals with toSignal().
  • Prefer Web Components when collaborating across frameworks or publishing design tokens.
  • Treat performance and a11y as acceptance criteria, not afterthoughts—measure early.

What’s next

Planned additions:

  • Signals + forms patterns (typed models & derived validity)
  • Router data with signals and granular prefetching
  • Hydration pitfalls checklist (SSR)
  • A dedicated “perf lab” comparing render strategies

If you want a pattern added, open an issue or PR. And if this helps, a ⭐ on the repo makes it easier for others to find.

Links

Built for the 2025 Angular ecosystem. Signals, NgRx, RxJS, Web Components, performance, and a11y—working together, not just in theory.

Similar Posts