angularsignalseffectangular-signals

I don't get the use case of angular signal effect function


What's the use case of effect function in angular signals?

It doesn't return anything and by default, you can't update other signals from the effect (I know you can disable this, but I assume it's not a good practice)... So in what cases is it useful? What's the best approach on fetching data from a server? Imagine you have a service that returns an observable and you want to call that service when a signal changes, what are the best practices?

Right now I'm doing it with code like this (some rx.js involved in this concrete example)

  protected options = toSignal(
    combineLatest([toObservable(this.timeFrame)]).pipe(
      switchMap(([timeFrame]) => {
        if (!timeFrame) {
          return of([]);
        }
        return this.myService.fetchData(timeFrame); // this returns an observable.
      }),
    ),
  );

Solution

  • Common use cases for effects are well described in official documentation

    • Logging data being displayed and when it changes, either for analytics or as a debugging tool
    • Keeping data in sync with window.localStorage
    • Adding custom DOM behavior that can't be expressed with template syntax
    • Performing custom rendering to a , charting library, or other third party UI library

    By definition it's a feature that makes you able to do any action when one of the signals that are used inside effect are changed.

    You wrote that it doesnt return anything but it is not correct. It returns EffectRef that can be used for cleanups as you do for observables/event listeners/intervals etc.

    I'd say the case with calling a service in effect may need a response and may not.

    If you don't need a response then there are no issues of just calling it in effect for example to track user actions when you just send request without waiting for response.

    If you need the data from server the there might also be different scenarios of how you gonna use it. It can be just for log, for alert message or you really need it to be used on UI.

    With first two (alert, log) it's clear that effect if totally fine.

    For the case with response data needed for template I'd dig a bit into how signals/effects work to understand if it's dangerous to use it.

    The docs says this:

    When not to use effects

    Avoid using effects for propagation of state changes. This can result in ExpressionChangedAfterItHasBeenChecked errors, infinite circular updates, or unnecessary change detection cycles.

    Because of these risks, setting signals is disallowed by default in effects, but can be enabled if absolutely necessary.

    They do not recommend doing state changes. But let's cover all these cases

    1. ExpressionChangedAfterItHasBeenChecked - this can happen due to the nature of effect

    Effects always execute asynchronously, during the change detection process. so if you do

    constructor() {
      effect(() => {
        this.isExceeded.set(this.prop() > 3)
      });
    }
    
    onClick(): void {
      this.prop.update(p => p + 1);
    }
    

    you should understand that change detection cycle gonna be triggered by zone.js whenever your onClick handler's execution is finished. effect as doc says will run asynchronously so it will be triggered somewhen during or after change detection that will cause this ExpressionChangedAfterItHasBeenChecked exception.

    1. "infinite circular updates" - I think you have an idea what it means. A small example, not a real world one since makes no sense but still
    effect(() => this.prop.set(this.prop() + 1))
    
    1. "unnecessary change detection cycle" - this gonna happen when you do e.g. subject.next() and use async pipe in template that will do markForCheck under the hood and start one more CD cycle

    In example you wrote I cannot really see any issues of doing something like this:

    protected options = signal([]);
    
    #sub: Subscription | null = null;
    
    constructor() {
      effect(() => {
        if(this.#sub) this.#sub.unsubscribe(); // cancel previous ongoing request if it's pending
        this.#sub = this.myService.fetchData(timeFrame).subscribe(response => {
          this.options.set(response);
        });
      }, { allowSignalWrites: true });
    }
    

    On the first glance when you see allowSignalWrites: true looks scary but if we dig into it and try to understand what can happen wrong with this code - I cannot find any potential issues with it because signal set is executed not directly in effect's root level but in a callback of another asynchronous operation which is service call here. So if you have zone.js enabled then change detection will start executing after your subscribe callback is executed, so signal will be already set before CD starts working. If no zone.js and using signal component approach then signal change will trigger change detection of component's view also on the next tick so no issues.

    So 0 smell of any of 3 problem points above are here.