Angular 2+/4/5/6/7: Smart, dumb and deeply nested component communication

MoMo picture MoMo · May 7, 2017 · Viewed 6.9k times · Source

NOTE: for simplicity consider the component depths as:

- Smart (grand)parent level 0
  - dumb child level 1
   ....
    - dumb grandchild level 2
      ....)

There are various options and conditions on how smart/grand/parent/child components communicate and pass data up and down a MULTI-LEVEL (at least 3 levels) chain. We'd like to keep our 'smart' (grand)parent component as the only component that has access to our data service (or atomic/immutable store) and it will drive exchange of information with 'dumb' (grand)children. The options we see are:

  1. Anti-pattern(?): Pass data down and up the component chain via @Input/@Output bindings. This is what some refer to as the 'extraneous properties' or 'custom event bubbling problem' problem (eg: here and here.) problem. No go.
  2. Anti-pattern: Smart component access to dumb (grand)children via @ViewChildren or @ContentChilden. This again hardwires the children and still doesn't create a clean mechanism for the (grand)children to pass data UP to the smart component.
  3. Shared message service as described in the angular.io cookbook here and an excellent post here.
  4. ?

Now in case of '3', the dumb (grand)children must have the message service injected. Which brings me to my questions:

Q1: It seems intuitively odd for each of the 'dumb' (grand)children to have a message service injected. Is best practice for the message service to be a dedicated service for this family OR does it piggy back on the data service the 'smart' grandparent is charged with mentioned above?

Q1A: Additionally, how is this much better than adding @Input/@Output bindings up and down the chain if all the components will have a service injected? (I see the argument that the 'dumb' component needs SOME way to get info)

Q2: What if the 'smart' grand parent were communicating with a redux-like store (ngrx for us)? Once again is the communication with the 'dumb' components best happen via an injected/dedicated messages service or is it best to inject the store into each 'dumb' component...or? Note, the inter-component communication is a combination of 'actions' (eg: form validation, disable button, etc) in addition to data (i.e. add data to/update store or service).

Thoughts greatly appreciated!

Answer

MoMo picture MoMo · May 9, 2017

(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.