34

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' (eg: here and here.). 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!

MoMo
  • 1,836
  • 1
  • 21
  • 38

3 Answers3

11

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

MoMo
  • 1,836
  • 1
  • 21
  • 38
  • 1
    The @Input/@Output approach prohibits you from introducing 3rd party components (examples: Angular's ``, or Angular Material's `...`) in between your parent and child. (More accurately, you can use @Input but not @Output; @Output events do not bubble.) – John C Jun 20 '18 at 16:50
  • This approach is bad when you have child components and you need to use `` for the child components. – DAG Nov 24 '18 at 19:50
  • I commonly face a problem where my child components are routable but I can make them dumb as well. Since activating them through router-outlet doesn't allow me to use of @Input/@Output, alternative is to use *ngIf to activate these components. Is it a good practice to use *ngIf to activate a component when route change in it's parent component because this allow me to create dumb component with @Input/@Output? – emkay Apr 08 '19 at 19:38
1

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
        }
    }
}
Frank Modica
  • 10,238
  • 3
  • 23
  • 39
  • Thanks for your thoughts. Note, again in terms of decoupling, IMO the top-level component shouldn't really care or know much about its grandchildren beyond providing indirect services which is its main responsibility. BTW, this isn't too different from option '2' in my original question. – MoMo May 09 '17 at 01:17
  • You're welcome. I think I get what you're saying - the top-level parent component now has to know not just what the child components need, but also their exact hierarchical structure. I am personally OK with this, because I see this kind of coordination as the parent component's job. Or I create a service to convert JSON into the hierarchical viewmodel which contains the presentational data / callbacks (good for unit testing). You're right, this is kind of like your 2nd option, but without actually touching the view children. Anyway, I'm curious to see how you end up solving this. Good luck! – Frank Modica May 09 '17 at 01:43
1

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

Muirik
  • 6,049
  • 7
  • 58
  • 116
  • I've added links of why it's best to decouple property bindings in the nested component chain. But plainly put, if one had a submit/clear buttons component nest 4 levels deep, whose entire raison d'etre was to emit a 'submit', why would the intermediary components, who have other separation of concerns, need to know or care? I agree with the prevailing opinion it's an anti-pattern. However, if the level depth were only 1, then I don't see a problem with passing data via @Input/@Outputs. – MoMo May 07 '17 at 19:29
  • Edited my answer to expand the pro/con discussion a little more and address the issues you raise. – Muirik May 07 '17 at 20:08