0

I have a user object which I observe in a membership service.

I want to update my services only if the user object has relevant changes.

To understand if my user object has relevant changes, I compare my local user object with the observed one. And always assign the new object afterwards.

This does not work, however.

export class MemberService {
  private subscription?: Subscription;
  user?: User;

  constructor(public auth: AuthService) {
    this.subscription = this.auth.user$.subscribe((user) => {
      const updateServices = this.hasUserRelevantChanges(user)

      // apparently this line always fires before the functioncall above?!
      this.user = user;

      if (updateServices) {
         this.updateMyServices();
      }
    });
  }

  ngOnDestroy() {
    this.subscription?.unsubscribe();
  }

  hasUserRelevantChanges(user: User | undefined): boolean {
      return user?.subscription !== this.user?.subscription ||
          user?.username !== this.user?.username ||
          user?.isBanned !== this.user?.isBanned;
  }

  updateMyServices(): void {
    // Updating my services!!!
  }
}

export class AuthService {
  public readonly user$: Observable<User| undefined> = this.user.asObservable();
  user: BehaviorSubject<User| undefined> = new BehaviorSubject<User| undefined>(undefined);

    constructor(private httpHandler: HttpHandlerService) { ... }

  handleUser(): void {
    this.httpHandler.login().subscribe(
        (user: User) => this.user.next(user));
  }

  updateUserData(newUser: User): void {
    this.user.next(Object.assign(this.user.value, newUser));
  }
}

How come that my function hasUserRelevantChanges() always compares the same, new objects? The local this.user always holds the new values already within this check, eventhough the assignment this.user = user comes afterwards?

So how can I understand if my new user object has the relevant values changed in comparison to the old/previous user-object?

Marco Bonelli
  • 63,369
  • 21
  • 118
  • 128

2 Answers2

1

Use RxJs operators to do so. You don't need to store user somewhere in your code but you can pipe it with pairwise and filter. So it would be something like this:

this.auth.user$.pipe(
           pairwise(), 
           filter(users => userChanged(users[0], users[1]))
           ... // other operators to handle change - tap, map etc
           )

It will be much cleaner if you move logic to proper operators.

Edit: Pairwise returns two values: previous and current, filter - as the name says filter events and emits next only if passed callback returns true

Edit2: You should implement userChanged or adjust your hasUserRelevantChanges which now you would need to pass 2 arguments.

Edit3: Why your code doesn't work? Because Object.assign doesn't create new object reference but change original object - so in fact, you are checking everytime the same object.

Update: Spread operator:

updateUserData(newUser: User): void {
this.user.next({...this.user.value, ...newUser});

}

KamLar
  • 428
  • 4
  • 8
  • Object shallow copy is a good point. If OP wishes to ignore the RxJS operators, they could try to make a rudimentary [deep object copy](https://stackoverflow.com/a/122704/6513921). – ruth Nov 21 '21 at 00:11
  • I tried to use pairwise filter but unfortunately the filter comparison would also just compare the same objects all the time, it does not store the value of the old object;( –  Nov 21 '21 at 01:03
  • Just remove `Object.assign` and use spread operator. It will create new object with updated values. I will edit my answer. – KamLar Nov 21 '21 at 09:01
0

"How come that my function hasUserRelevantChanges() always compares the same, new objects?"

That's because you're always assigning the new values to the this.user variable. Ideally you'd need to update it with the new user only if it's different. In other words, it must be moved inside the if block.

this.subscription = this.auth.user$.subscribe((user) => {
  const updateServices = this.hasUserRelevantChanges(user)

  if (updateServices) {
    this.user = user;
    this.updateMyServices();
  }
});

Update: pairwise + filter

In your scenario, you would do better to use the RxJS pairwise + filter operators to check the condition.

this.subscription = this.auth.user$.pipe(
  pairwise(),
  filter(([user1, user2]) => (
    user1?.subscription !== user2?.subscription ||
    user1?.username !== user2?.username ||
    user1?.isBanned !== user2?.isBanned;
  ))
).subscribe(([user1, user2]) => {    // <-- Update 2: ignore `user1`
  this.updateMyServices(user2);
);

If you're actually comparing all the properties of the object with it's previous values, you could replace the entire pairwise + filter with the distinctUntilChanged operator.

ruth
  • 29,535
  • 4
  • 30
  • 57
  • But I make the `hasUserRelevantChanges(user)` call BEFORE the assignment is done, how come the function acts like if the assignment was done already?:/ –  Nov 20 '21 at 23:57
  • @TeaCup: I'm not exactly sure what do you mean by the assignment is done already. What makes you believe so? On the other hand, I've updated the answer using RxJS operators `pairwise` + `filter` that IMO is better suited to your requirement. – ruth Nov 21 '21 at 00:04
  • I have made the changes as proposed and now use pairwise filter, but I still receive twice the same object every time I make changes;( –  Nov 21 '21 at 01:02
  • @TeaCup: Indeed, using `pairwise` would emit two values, previous and current. As shown in `filter(([user1, user2]) => ()` statement you could get both users in the subscription callback and ignore the `user1` which is the previous value. I've updated the answer. – ruth Nov 21 '21 at 09:54