← Retour au blog
websocketsrxjsreal-timeangularreactive-architecture

WebSockets and Real-Time in Angular: Beyond Polling, a Solid Reactive Architecture

WebSockets provide persistent bidirectional communication, but integrating them into a complex Angular application poses architectural challenges that are often underestimated. Too many projects simply wire a WebSocket directly into a component, listen for messages, and call changeDetection.markForCheck(). The result: a fragile application, difficult to test, with connections hanging mysteriously and chaotic state synchronization. This naive approach quickly creates massive technical debt. True real-time architecture requires thinking in terms of data streams, resilience, and explicit connection lifecycle management.

Architecting the Real-Time Layer with RxJS

The first decision is to never expose the WebSocket directly to components. Create a dedicated service that encapsulates connection logic and exposes RxJS observables. This service becomes the single point of control for all real-time events. For example, a NotificationService can expose a notifications$ observable that automatically emits each received message. RxJS provides powerful operators to transform these raw streams into stable application data. The shareReplay() operator is critical: it caches the last message and allows multiple subscribers to share the same WebSocket connection without duplicating it. Without shareReplay(), each new component that subscribes would create a new WebSocket connection—a classic resource leak.

export class RealtimeService {
  private socket$: Observable<WebSocket>;
  private subject = new Subject<Message>();

  notifications$ = this.subject.asObservable().pipe(
    shareReplay(1),
    catchError(err => {
      console.error('Notification stream error:', err);
      return throwError(() => err);
    })
  );

  constructor() {
    this.socket$ = this.createSocket().pipe(shareReplay(1));
  }

  private createSocket(): Observable<WebSocket> {
    return new Observable(observer => {
      const ws = new WebSocket('wss://api.example.com/ws');
      ws.onopen = () => observer.next(ws);
      ws.onerror = err => observer.error(err);
      return () => ws.close();
    });
  }
}```

This approach centralizes connection logic and allows RxJS to orchestrate subscriptions. Components remain decoupled from WebSocket mechanics and can focus on displaying data. The service transparently manages connection lifecycle: when the last subscriber unsubscribes, the connection closes automatically.

Handling Reconnections and Resilience

WebSocket connections fail—it's a certainty. Mobile networks switching between WiFi and cellular, proxies killing idle connections after 30 seconds, servers restarting. Production applications must handle these cases gracefully. RxJS's retryWhen() operator lets you define an exponential backoff reconnection strategy: wait 1 second, then 2, then 4, with a reasonable ceiling. This approach avoids overwhelming the server during a massive outage. Combine it with takeUntil() to stop retry attempts when the component is destroyed.

private createSocket(): Observable<WebSocket> {
  return new Observable<WebSocket>(observer => {
    const ws = new WebSocket('wss://api.example.com/ws');
    ws.onopen = () => observer.next(ws);
    ws.onmessage = (event) => this.subject.next(JSON.parse(event.data));
    ws.onerror = () => observer.error(new Error('WebSocket error'));
    ws.onclose = () => observer.error(new Error('WebSocket closed'));
    return () => ws.close();
  }).pipe(
    retryWhen(errors => errors.pipe(
      concatMap((error, index) => {
        const delayMs = Math.min(1000 * Math.pow(2, index), 30000);
        console.log(`Reconnecting in ${delayMs}ms...`);
        return timer(delayMs);
      })
    ))
  );
}```

The key is transforming errors into reconnection opportunities, not defeats. Display a visual indicator: a small icon or bar showing connection state. Users must know the application is attempting to reconnect. Without this feedback, they assume the app is broken and refresh the page—making things worse.

State Synchronization and Conflict Management

When data arrives in real-time and users can also modify state locally, conflicts become inevitable. Someone else adds a task while you're editing one. The naive approach: simply replace local state with the server's. This creates a terrible experience: the user types text that suddenly vanishes. A better strategy uses optimistic queuing. You apply local changes immediately but also send a message to the server. If the server rejects the change, you undo it locally. If the server accepts, you're already done—state is already correct.

Angular 19+'s Signal() simplifies this management. Store state in a signal, expose it as an observable with toSignal(). When a message arrives from the server, intelligently merge it with local state. A map() operator can check whether the remote change conflicts with a pending local operation. If so, ignore it or ask the server to replay your change after the remote one. This creates quasi-transparent synchronization without data loss.

Common Pitfalls and Anti-Patterns

The most common pitfall is calling markForCheck() or detectChanges() manually after each WebSocket message. This signals that your Angular change detection isn't working properly. If you must mark manually, you have a zone or async problem. Always use RxJS with the async pipe or signals—Angular will automatically detect changes. Second pitfall: creating a service that only exposes a subscribe() function rather than an observable. This prevents using RxJS operators and creates sticky code in components. Third pitfall: not cleaning up subscriptions. Every time a component is destroyed, observables must terminate properly. Use takeUntil(destroy$) systematically.

Fourth pitfall: assuming the WebSocket is always connected. Test what happens when the connection is closed. Does your application display a clear message? Continue working in read-only mode? Or crash spectacularly? Most applications never test this scenario and fail catastrophically in production.

Conclusion: Real-Time, Not Magic

Implementing real-time in Angular demands architectural rigor. Encapsulate WebSockets in an RxJS service, manage reconnections with exponential strategies, synchronize state intelligently, and test failure scenarios. Don't search for a universal solution—each use case (notifications, chat, real-time collaboration, financial data) has its own requirements. But principles remain: a testable, decoupled, and failure-resilient real-time layer. With this foundation, you'll build reactive applications without maintenance nightmares.

Développeur Angular & Mobile freelance — Strasbourg.

© 2026 Emilien Pons — Tous droits réservés.Conçu avec Angular, PrimeNG et ❤️