angularangular-services

Angular Reactivity and Communicating via Services


Almost exclusively if you search for examples of cross-component communication via services the examples are using RxJS inside the service (could be signals now too). However it doesn't seem like this is necessary. For example, even in the case of the below service, both components will be able to see changes to counter without using RxJS or Signals.

Is there a downside to not using RxJS or Signals in this case? I recognize that using RxJS or Signals is kind of the prescribed way to do things in Angular and that's a good reason to do so, but I'm wondering if there's an actual reason beyond that. Would this cause any sort of issue or bug?

Sample service below and here is a sample Stackblitz.

Service:

import { Injectable } from '@angular/core';

@Injectable({
  providedIn: 'root',
})
export class MyService {
  private counter: number = 0;

  increment() {
    this.counter += 1;
  }

  getValue() {
    return this.counter;
  }
}

Component1:

import { Component } from '@angular/core';
import { MyService } from './MyService';

@Component({
  selector: 'component1',
  standalone: true,
  template: `
    <h1>Component 1</h1>
    Current Value: {{ valueFromService }}
    <button (click)="increment()">Click to increment</button>
  `,
})
export class Component1 {
  constructor(private myService: MyService) {}

  get valueFromService() {
    return this.myService.getValue();
  }

  increment() {
    this.myService.increment();
  }
}

Component 2:

import { Component } from '@angular/core';
import { MyService } from './MyService';

@Component({
  selector: 'component2',
  standalone: true,
  template: `
    <h1>Component 2</h1>
    Current Value: {{ valueFromService }}
  `,
})
export class Component2 {
  constructor(private myService: MyService) {}

  get valueFromService() {
    return this.myService.getValue();
  }
}

Solution

  • Using Observables plays well with OnPush ChangeDetectionStrategy that usually has a positive impact on runtime performance. It also makes it simplier to work with derived states, handle multiple simultanious updates, and running side-effects as a reaction to state updates.

    Your example works as long as components use a default ChangeDetectionStrategy. Let's take a closer look at a very simplified and not very precise explanation of what happens when a user clicks the increment button:

    1. A 'click' event is triggered
    2. A task for the associated event handler is added to the Event Loop
    3. When the browser decides that it is time to run stuff from the Event Loop it runs the handler so it changes the state in the service
    4. Zone.js (a library, Angular uses internally for watching out for otherwise opaque Event Loop state) notifies Angular that the Event Loop queue is empty and Angular runs a Change Detection
    5. During the Change Detection cycle Angular re-renders components and this is when the component pulls the new value from the Service

    So the service value update and rendered value update are kind of separated. If the value updates as a result of something that Zone.js cannot intercept (e.g. awync/await) the view won't reflect changes. It is worth to note that Events may happen quite often and cheking every single component on each event may become quite a heavy task. That is why it is considered a better practice to use OnPush ChangeDetectionStrategy.

    Not going too deep into details, when OnPush is used, Angular skips checking this component and all its children unless some of the inputs are changed, or the component is marked for checking via e.g. calling ChangeDetectorRef.markForCheck(). It is a common pattern to taking an observable from a service and bind to it in template via async pipe and the pipe calls markForCheck each time a the Observable emits a value. So while Change Detection cycle is still needs to be triggered, it is an Observable value update makes a component checked, and those components that have no changes won't be checked at all.

    Here is a demo

    As for derived states and side effects, let's imagine we have two services A and B, and when a value updates in A need to do something in B. Without Observables that would require injecting B to A and what if we have dozens of such connections? It gets messy pretty fast. The same is fair fo the components, it is pretty common situation that before rendering the value it should be transformed, combined with another one, and only then rendered. While this can be done inside a getter it is a waste of resources to run them on each CD cycle.

    The last thing is that some of Angular's APIs come as Observables, for example, reactive forms, router events, HttpClient so it is simplier to fit them into your own stuff when it is also Observable-based.