Angular 2+/4/5/6/7: Smart, dumb and deeply nested component communication
(UPDATE: 02-07-2019: This post was getting dated--added the 'store/ngrx' pattern)
So after looking into this further, when it comes to how best to communicate down and up a nested component chain, there seems to be really only two options -- a Faustian bargain between:
EITHER
- either pass @Input/@Output bindings up, down, and throughout the nested component chain (i.e. deal with the problems of 'custom event bubbling' or 'extraneous properties')
OR
- Use a messaging/subscription service to communicate between this family of components (great description here) and inject that service for each component in the chain.
OR:
- The reactive store pattern (e.g. 'ngrx') is another option. Note, IMO, the notions of smart and dumb components still apply. Namely, dumb components never access the store directly. Again, the smart components are the main party to get data via the store.
I'm personally a proponent of utilizing smart and presentational ('dumb') components. Adding a 'store' should also be done selectively as it significantly increases the costs of the process ranging from architecture, consistent implementation patterns, development, and maintenance to on-boarding of new personnel. Nominally, a 'dumb' component only needs @Inputs and @Outputs and that's it. It does not care how deep or shallow it is in a component tree--that's the applications problem. In fact it doesn't care what application uses it in the first place. Meanwhile, a deep down component isn't very dumb or transportable if an application specific service is injected into it. BTW, the counter-part 'smart' component is really providing intermediary services (via a first class @Injectable service or redux-like store) to whichever dumb component in its family tree that needs it. The smart component also doesn't care about components beyond its immediate child's @Inputs as long as the grandchildren somehow signal up a service/store action needs to be taken (again via the @Input/@Output chain). This way a smart component also becomes transportable across application lines.
Given this, the Faustian bargain, IMO, leans towards utilizing an @Input/@Output chain with all the mentioned issues it brings with it. That said, I'm keeping an eye on this and welcome clean and decoupled alternatives if anyone knows of any.
Input() and Output() bindings are also a perfectly legitimate way to handle this. Let the smart component handle the logic of generating the values, and then use Input() and Output() to simply pass and receive the values along the component chain.
Of course, this points to one of the downsides of the smart/view approach: more files; more boilerplate. That's why I wouldn't argue for a single approach that's one-size-fits-all. Rather, choose an approach that makes sense in your current context (both for the app and for your organization).
Why is #1 an anti-pattern? The grandparent component owns the data and passes it down to the dumb child components via @Input parameters. The dumb child components simply invoke callbacks when an event occurs (via @Output event emitters), causing the grandparent component to manipulate the data. Seems clean to me.
Edit: I see your point about repeatedly passing values like a submit handler through many intermediate layers. Maybe a nested structure which represents your component tree could be created in the parent component. Then each component can be passed the properties it needs, plus an object to pass down to the next component. Each component then only knows about the one below it:
// Parent component builds this object (or gets a service to do it)
viewModelForChildComponent: {
property1NeededForChildComponent,
property2NeededForChildComponent,
viewModelForGrandChildComponent: {
property1NeededForGrandChildComponent,
property2NeededForGrandChildComponent,
viewModelForGrandGrandChildComponent: {
property1NeededForGrandGrandChildComponent,
submitHandlerNeededForGrandGrandChildComponent
}
}
}