Circular call to cause stack size exceed

Viewed 123

I have two sibling components and I want to share the data between them. For the user's convenience, if the UI on component one changed then I want to emit the inform the component two. Then the component two's UI should change by the passed parameter.

Vice versa if the component two's UI change I also want to inform component one as well. So I used Behavior subject to share the data.

However it causes the circular calling.

Stackblize BehaviorSubject demo

The BehaviorSubject is in the service class.

import { Injectable} from '@angular/core';
import {BehaviorSubject, Observable} from 'rxjs';

@Injectable()
export class UsersService{
  constructor(){}
  private user = new BehaviorSubject<number>(0);

  public getUser = (): Observable<number> => {
   return this.user.asObservable();
 }
  public editUser = (newUser: number) => {
   this.user.next(newUser); 
 }

}

So we have get and set part. In the sibling components we call get user part in ngOninit.

 ngOnInit(){

 this.userService.getUser().subscribe(u => {
    if(u > 0) {
       this.patchData(u);
       this.newUser = u + 1;
       console.log(u);
      }
    });
  }

Once we get the user number then we emit so the other component can receive it and render the UI afterwards.

The error is shown in console.

3 Answers

Your logic is indeed circular!

The main point I think you were missing is that the service is the single source of truth for your user value. When it changes, subscribers are automatically notified because the newest value is pushed the the observable subscription created from the getUser() method.

There is no need to both components to push updates to the service again. Component #1 can simply call the userService.editUser() method, which causes the service to update it's internal state, which automatically pushes the new value out to subscribers.

I've got it working for you in this stackblitz.

Also, you'll notice I leveraged the async pipe in the template instead of subscribing in the controller. There are many benefits of doing this, one of them being simpler code :-)

I think the problem is in the architecture. In this case, the each component is storing its own state, on which other component are sort of dependent as well. I think the best approach is to have a single source of truth and let that source to update the state and dispatch it to the consumers, which can select slices from it.

For example, if you want to keep user state in one place, you could modify your UserService like this:

class UserService {
  private userSource = new Subject<Partial<User>>();

  user$ = this.userSource.asObservable();

  constructor () {
    this.user$ = this.userSource.pipe(
      // It's important that we return a new reference(e.g for the async pipe)
      scan((acc, crt) => ({ ...acc, ...crt }))
    )
  }

  addUser (u: User) {
    this.userSource.next(u);
  }
}

and now you can have N consumers. I think this approach also solves another problem, which is the fact that you mixed the logic of updating the specific(inner) state and the logic of dispatching state for other consumers. For example

this.patchData(u); // Notify other consumers
this.newUser = u + 1; // Update inner state

A data consumer(e.g component) would look like this:

class DataConsumer {
  get user$ () {
    return this.userService.user$;
  }

  constructor (private userService: UserService) { }

  // Updating the user only when needed(e.g user input)
  changeUser (name) {
    this.userService.addUser({ name });
  }
}

and the view:

{{ (user$ | async) as user }}
Related