0

I have a parent component with two children.

The parent component has a property which is actually a dictonary like this:

dict = {a: 0, b: 0, c: 0};

I do input binding so that both of the child components can have access to dict.

<app-child-a [dict]="dict"></app-child-a>
<app-child-b [dict]="dict"></app-child-b>

Child A changes a property of dict

@Input() dict: any;

someFunctionInChildA() {
  this.dict.b = 1;
}

I can verify that the Parent component knows about this change.

ngAfterInit() {
  setInterval(() => console.log(this.dict), 1000);
}

This prints 0s until someFunctionInChildA is triggered after which it prints 1s.

Here's my problem: child B doesn't seem to see the change. Here's some code from child B

@Input() dict: any;

ngAfterInit() {
  setInterval(() => console.log(this.dict), 1000);
}

This prints 0s all the time, even after someFunctionInChildA has been triggered.

Do I not understand how property binding works? Or is it more likely that I have a silly or unrelated bug elsewhere?

Alexander Soare
  • 2,825
  • 3
  • 25
  • 53
  • in child 2 if you increase the interval to 1500ms...do you see 1's? – Ramesh Reddy Dec 13 '19 at 16:41
  • 1
    i see no reason this shouldn't work, other than `ngAfterInit` not being a life cycle hook of any kind... but this is VERY bad practice in an angular context. object mutation at a distance leads to very hard to understand code and vert hard to understand bugs. which is what you may be experiencing – bryan60 Dec 13 '19 at 16:43
  • @Ramesh changing the interval doesn't change anything. I found the bug because it was breaking the intended behaviour. The setInterval is just there for debug. – Alexander Soare Dec 13 '19 at 16:45
  • @bryan60 Thanks for the tip. I had a bunch of properties which all related to one thing being passed around between components so I thought I'd be smart and put them all into a dictionary. For my use case, is there a better way to do it. Maybe if I kept that dictionary in a service and all components who need to know about it talk to it directly? And maybe using a setter and getter instead? – Alexander Soare Dec 13 '19 at 16:47
  • 1
    you should use a service and rxjs to share an object and make changes to it and keep other components notified – bryan60 Dec 13 '19 at 16:49
  • @bryan60 thanks so much! even though I'm sad to hear it, as I access that dict about 40 times in my code and I thought I was going to deploy today ... :( – Alexander Soare Dec 13 '19 at 16:50
  • Did you try ngOnChanges(){} in the componentB ? – Durgesh Pal Dec 13 '19 at 16:51
  • @DurgeshPal no I didn't. I proactively access `dict` when I need it, so I don't know if that would help. I could try, but maybe it's worth implementing @bryan60 's suggestion if I'm going to make changes. – Alexander Soare Dec 13 '19 at 16:53
  • @AlexanderSoare as bryan said using a service is best, I thought you were just experimenting with input binding. Anyway implementing a service way is easy shouldn't take too long even if you have to change 40 occurrences. – Ramesh Reddy Dec 13 '19 at 16:59
  • I have checked, ngAfterContentChecked is working fine for componentB. – Durgesh Pal Dec 13 '19 at 20:36

2 Answers2

2

as you can see here: https://stackblitz.com/edit/angular-7-master-nxys2f?file=src/app/hello.component.ts

the code in question works fine. BUT.. this is a very bad practice. as this sort of at a distance mutation creates hard to reason about code and hard to understand bugs. I'm guessing your real application is a bit more complex, and you're probably accidentally breaking the reference somewhere. This is why you should use a shared service model and rxjs to share information between components:

@Injectable()
export class DictService {
  private dictSource = new BehaviorSubject({a: 0, b: 0, c: 0})
  dict$ = this.dictSource.asObservable()
  setDict(dict) {
    this.dictSource.next(dict)
  }
}

then inject into your components and subscribe:

constructor(private dictService: DictService) {
  this.dictSub = this.dictService.dict$.subscribe(dict => this.dict = dict) // don't forget to unsubscribe!
}

and use the service to update:

someFunctionInChildA() {
  this.dict.b = 1;
  this.dictService.setDict(this.dict)
}
bryan60
  • 28,215
  • 4
  • 48
  • 65
  • thanks again for the additional clarification. I've answered my own question now. Turns out I was doing something with rxjs, but also left behind some logic which was trying to mutate the dictionary directly within the component. Probably if I was more aware of what you're demonstrating here, I wouldn't have made the blunder. – Alexander Soare Dec 13 '19 at 17:06
  • yea these kinds of bugs are why you avoid this. apps get very messy very fast if you make a practice of this – bryan60 Dec 13 '19 at 17:06
0

I've figured this one out, and I doubt anyone else will find this useful as it's a rare thing to happen...

A couple of days ago I actually made a service which would save and update dict into Firebase, and this would sync up with my components via rxjs. But I left some of the old logic behind (that is, the logic whereby dict was being managed by the component rather than the service).

I will mark this as the correct answer, but the real credit goes to @bryan06 for his good explanation of the correct pattern for this sort of application.

Alexander Soare
  • 2,825
  • 3
  • 25
  • 53