Search code examples
angularrxjsside-effects

RXJS side-effect execution order


I have this huge object that I want to be able to set with a one simple function like below

  private setPerson = (partialPerson: Partial<Person>) => {
    this.person$.next({ ...this.person$.value, ...partialPerson });
  };

But when some of the properties changes I want to be able to run a side-effect like this

  personNameEffect$ = this.person$.pipe(
    map(({ name }) => name),
    distinct(),
    tap((name) => {
      // Do some complex calculation 
      this.person$.next({ ...this.person$.value, firstName: name });
    })
  );

  constructor() {
    this.personNameEffect$.subscribe()
  }

This works like I want it to, but the issue is the execution order of these steps, this is what I think is happening:

  1. setPerson function is called with a partial object
  2. The side-effect is ran and setting the values (Keeping old value from call before)
  3. The next inside setPerson is ran and overwriting what the side-effect have changed

How can I fix this execution order? I want the next to set the values and then the side-effect to run. Here is a Stackblitz link to a playground where this can be reproduced in.


Solution

  • Your issue is likely related to the nested next calls and/or convoluted logic flow. Furthermore getting the value out of a BehaviorSubject using value is generally frowned upon as its not reactive.

    I have modelled your problem in a more reactive flow ie:

    Use the BehaviorSubject only for pushing updates rather than a complete Person. Therefore no longer using it as a store, and avoiding having to update itself:

    personUpdates$ = new BehaviorSubject<Partial<Person>>({});
    

    person$ stream accumulates these updates, and effectively stores the 'current' person:

    person$ = this.personUpdates$.pipe(
        scan((acc, curr) => ({ ...acc, ...curr }), {
          name: '',
          firstName: '',
          lastName: '',
        } as Person)
      );
    

    Because of the change to the BehaviorSubject, the effect no longer needs to access it's value:

    personNameEffect$ = this.person$.pipe(
        map(({name}) => name),
        distinct(),
        tap((name) => {
          console.log('Name changed detected');
          this.personUpdates$.next({ firstName: name });
        })
      );
    

    Finally setName and setLastName are simplified, again due to the the BehaviorSubject only requiring a partial value.

      setName = (name: string) => {
        this.personUpdates$.next({ name });
      };
      setLastName = (lastName: string) => {
        this.personUpdates$.next({ lastName });
      };
    

    Stackblitz: https://stackblitz.com/edit/angular-module-sandbox-kzexrb?file=src%2Fapp%2Fapp.component.ts,src%2Fapp%2Fapp.component.html