2

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.

Community
  • 1
  • 1
Hello
  • 796
  • 8
  • 30

3 Answers3

0

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 :-)

BizzyBob
  • 12,309
  • 4
  • 27
  • 51
  • Your code assume that I change the component one then notifying component two. It works. Suppose I have button in component two, If I click it then I also want to inform component one. How? – Hello May 15 '20 at 20:12
  • that's no problem. You just call the `userService.editUser()` method. – BizzyBob May 15 '20 at 20:20
  • the way it works is, 1) the components subscribe to the public observable property `user$` of the service. 2) When components want to update the value, they call the service method that updates that state internally. 3) The components themselves are automatically notified of new values because they are subscribed. – BizzyBob May 15 '20 at 20:23
  • Can you assign the number type value from the other sibling component to a new variable in component? I want to see the result or log it. Many thanks. – Hello May 15 '20 at 20:51
  • I don't understand what you are asking: "assign the number type". To log results as they are emitted through the observable stream you can tack on a `.pipe(map())` to the assignment of `public user$ = this.user.asObservable().pipe(map(console.log))` in the user service. Does that help? – BizzyBob May 17 '20 at 14:46
  • I am not sure how to use `pipe(map`. Can you paste your whole code? – Hello May 18 '20 at 12:56
0

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 }}
Andrei Gătej
  • 11,116
  • 1
  • 14
  • 31
  • I think that perhaps I can create a static variable to store the value. Then in custom components's ngOnInit we retrieve it and in action part we change it. Do you think is that okay? – Hello May 16 '20 at 14:43
  • Why does it have to be static? – Andrei Gătej May 16 '20 at 14:45
  • Anyway, it is okay if it is not static. But I can use a variable to store it? – Hello May 16 '20 at 14:48
  • You mean a variable, like, not an observable? If you need to have _direct_ access to the current state of the observable, you could use a `BehaviorSubject` instead. – Andrei Gătej May 16 '20 at 14:50
  • Yes, I just want a number. The value can be shared among components. – Hello May 16 '20 at 14:54
  • If you decide not to use an observable, you might find out that it's pretty cumbersome to keep the other consumers up to date with the current state. You can still use the describe the approach described above, by using numbers instead of objects. – Andrei Gătej May 16 '20 at 14:56
  • What does it mean `...` before acc? I meant `scan((acc, crt) => ({ ...acc, ...crt }))` – Hello May 18 '20 at 18:06
  • It's the [spread operator](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_syntax). – Andrei Gătej May 18 '20 at 18:15
0

I used another way. Likely Get current value from Observable without subscribing (just want value one time)

In data service I just return the current value rather than using subscription.

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

public getUser = () => {
   return this.user.value;
}
public editUser = (newUser: number) => {
   this.user.next(newUser); 
}
Hello
  • 796
  • 8
  • 30