The problem
Say you are working on an e-commerce application and are tasked with creating a product page that renders a call to action button to buy awesome Nike shoes.
Right now, the app has a following component tree:
> product-page
>> card
>>> card-content
>>>> cta-button
You want to render "Buy Air Max" as a label of the call to action button.
The issue is the button component is deeply nested in the component tree, and the product model information is available in the top most component.
> product-page <-- Here we know the product name
>> card
>>> card-content
>>>> cta-button <-- Here we want to render the product name
The Struggle
You might jump in and create the @Input()
in each component and pass it down to the button component.
<!-- product-page.component -->
<card [productName]="productName"></card>
<!-- card.component -->
<card-content [productName]="productName"></card-content>
<!-- card-content.component -->
<cta-button [productName]="productName"></cta-button>
<!-- cta-button.component -->
Buy {{ productName }}
It works fine, but there are couple of issues:
- We are adding
@Inputs()
to components which don't use them, only passing them down to the child components.
> product-page
>> card <-- I don't care about the product name
>>> card-content <-- I don't care about the product name
>>>> cta-button
- It makes adding additional
@Inputs()
painful. If button needs more data from the product page, you have to pass it through two other components. This process is sometimes referred to as prop drilling.
> product-page <- We know the product price
>> card <-- I have to add @Input() for price
>>> card-content <-- I have to add @Input() for price
>>>> cta-button <-- I have to add @Input() for price
- It makes unit testing more difficult as you have to test passing the
@Inputs()
in each component.
The Solution
You can solve this problem using a different approach. How about we use content projection instead of drilling props with @Inputs()
?
Compare the previous solution, with the following:
<!-- product-page.component -->
<card>
<card-content>
<cta-button>Buy {{ productName }}</cta-button>
</card-content>
</card>
<!-- card.component -->
<ng-content></ng-content>
<!-- card-content.component -->
<ng-content></ng-content>
<!-- cta-button.component -->
<ng-content></ng-content>
This approach has the following benefits:
- We no longer add
@Inputs()
to components that don't need them so we avoid prop drilling. - Components become more extensible. You can pass as much information to the button component as you'd like, without touching the card components.
- Because of the previous points, unit testing becomes much easier.
The Benefits
Let's see how we might benefit from this approach.
Say you are now tasked with extending the call to action button with a price tag - "Buy Air Max at $199".
With content projection approach, we only need to make a small change in the product page component:
<!-- product-page.component -->
<card>
<card-content>
<cta-button>Buy {{ productName }} at {{ productPrice }}</cta-button>
</card-content>
</card>
That's it! See how easy it is? No props drilling, no test changes for the child components, no problem :)
Hope you're having a great one, and I'll see you for more web dev posts in the future 🥳
Top comments (0)