(originally posted on Medium)
Component architecture is pretty hard! Without strict discipline, you can fall into really bad habits. You forget everything you know about building software, and write confusing, unmaintainable code. The application started small, but then it grew and the requirements changed… Now it’s out of control! 🔥🦖🔥
Inevitably, there comes a point where our components are far too big, and we need to break them down a bit. But is there a way that we can redesign and refactor our components for long term flexibility? I think the answers lie in design patterns like Dependency Injection, and Inversion of Control!
Can’t be bothered reading the whole article? Here’s the gist in a handy tweet sized snippet:
Want more details? Read on… ❤️
Let’s imagine a card component like the following:
How can we implement it in a way that means it will be easy to modify as our requirements change?
Here’s an initial implementation of the card in StackBlitz! I’m going to stick with Angular for all these examples, but the concepts can apply to any Framework! Jump in and take a look at how it works:
Here's the HTML template for a first attempt at the card component. It contains everything that our component needs to render correctly.
This is already quite a large component! We know that large components can be difficult to change. One way that we could make the card more maintainable is by splitting the card up into a few smaller components.
It might make sense to break our
<my-card> component into three parts:
And then update our card component to use the new components:
Here’s a StackBlitz implementation of the more modular card:
This is better! It looks the same, but there’s a clearer separation of responsibilities. This card is definitely going to be easier to reason about. Job well done 🎉! But…
What happens when we get a bit further down the road, and some of our requirements change. What if we have to handle an image carousel rather than one single image?
One approach might be something to add some more logic to the template so that it can handle a single image or many images. That might look something like this:
We’ve added the required functionality, and it doesn’t seem too awful. But then, once again, our requirements change and now we have to handle a video in our card…
We could add another
*ngIf (even more logic) and move on, but it starts feeling pretty gross:
Let’s see what that looks like:
This isn’t a particularly flexible way to add functionality to our card component. And unfortunately this will have a performance impact too! Our application bundle will include the code for all three different situations — even if we only use one at a time. As well as being inflexible, we’ve now got quite a bit of logic in our template. In the Angular world, we might want to move this logic to a service, and then write some tests to make sure we don’t break it in the future… but that still doesn’t sound great!
Let’s take a step back and think about what’s going on… 🤔🤔🤔
The problem we have is that
There’s nothing super bad about this, but it’s inherently inflexible. What happens if we want to swap out the engine for an electric motor? Or replace the automatic transmission with a manual one?
The usual way to handle this kind of thing is to use a pattern called “Inversion of Control”. The Angular framework heavily relies on the IoC pattern. The constructor of a class describes the shape (or
interface) of its dependencies. The framework’s powerful Dependency Injection system handles the rest:
Now we can swap out our dependencies as we like, as long as they match the required interfaces! This makes our code much more flexible, and more testable. 🎉🎉🎉
So how do we apply this same inversion to our template code?
Another way that we could re-architect the
<my-card> component is by adding content slots. We do that with one of my favourite Angular features,
<ng-content>. Each slot is analogous to an argument for the “constructor” of the component template. The
select attribute is like the
interface — it defines which content is inserted into which slot:
I’m a huge fan of this article by Eudes Petonnet that provides a tonne more information about
<ng-content>! Check it out!
Now that we’ve got out code set up to use
<ng-content>, we can use
<my-card> like this:
And if we have to swap out our image for a video we can use a different component in the slot:
Here’s yet another StackBlitz implementation, showing the card with
<my-card> component is now much simpler!
It now only defines the layout of the group of slots. Each of the inner components has to be able to take up all the space available to it. It is exactly like our TypeScript example! Except instead of a metaphorical shape (the interface), we have an actual shape that we need to fill. We’ve been able to split the HTML and CSS so that each component handles how it appears on the page. You might be thinking that it is quite verbose to repeat the whole structure over and over? Remember that we can still make a reusable wrapper component that encapsulates all the slots (for example, a
<ng-content> pattern gives more flexibility in finding the right level of abstraction.
We’ve simplified the TypeScript too! The
<my-card> component no longer needs to know about data needed to render the card. The data is instead managed by the component that is constructing the contents for each slot. Instead of the
<my-card> component being the orchestrater, the inner components receives the data. This is one of the most powerful parts of this pattern, which is that we’ve pushed most of our components further towards the “presentational” end of the component spectrum. Most of our components do very little. And we no longer have to pass data down through multiple layers of components.
Of course, this is a trade off. We have a lot more files that we began with. We had to define new directives for each of our slots so that the Angular compiler can understand them. And when we use the component, the markup is also more complicated. We have more components than we had before, which results in more DOM nodes. Excessing DOM nodes can have a compounding performance impact in a large application.
Is there anything we can do to reduce the complexity of using the component?
Here’s the final StackBlitz, if you want to see all the code:
We’ve removed the extra elements, and the extra directives that define the content slots. Is this a better API? Is it clearer? Maybe! Maybe not! I’m not super sure. But it’s important to play with things like this when we’re designing component APIs. What do you think? Did we go too far? Not far enough? Please leave a comment, or tweet me and share your thoughts!
I’m a big fan of this pattern! I love how it aligns with the dependency injection pattern with TypeScript in Angular. I also love how the logic and moving parts melt away as we re-architect our code. It's great for anywhere where you need reusable components, such as in a component library. But I think it’s been particularly unused in applications!
Like most things, it’s a trade off, and if you’re not careful it can be a premature abstraction. But it’s definitely a useful tool to have in your toolkit, and I hope you find a need for it.
So please, try it out in your framework of choice! Build a few components that use content slots and let me know how you get on!