How to write good, composable and pure components in Angular 2+

Most of us know what Smart and Dumb components are. We know we should use @Input() and @Output() as much as possible. But when our SPA gets big enough, it starts to remind us more and more of a typical spaghetti and it seems we cannot even help it.

Jack Tomaszewski
Jack the Nomad

--

The reason is very often we know what are the good and bad patterns in the code development, but, especially in the Front-end, we often get confused with what we are ought to and not ought to do. The patterns we should follow start to get blurry and we end up using shortcuts in our code more often than we don’t.

One of such patterns is to split your components into “Smart” and “Dumb”. It says that we should keep all business logic and side effects in the Smart components while making all the other components as “Dumb” as possible.

This works well but unless we enforce some strict rules in our project, we will forget about it. Because when the pattern is not a rule and is just “a general idea” that we keep in mind, eventually we will end up skipping it more and more.

For example, we start having our components Dumb, but when we feel bloated with the need of passing and handling all the inputs and outputs everywhere, we get lazy and just mutate the inputs’ data directly.
(This will make it difficult to guess what component is changing what data and when.)

Other time we want to “optimize” the performance of our components, and instead of depending on inputs and outputs, we just take the “ng1 approach”, which means we create a singleton Controller service that every component will just inject to itself whenever it needs anything.
(This will make it difficult to reason about what components receive what data.)

A few more cases of taking shortcuts like that and we start to notice that strangely the Angular’s Change Detection mechanism doesn’t apply changes in our components (when normally it should). So we add a few this.cdRef.markForCheck() lines here and there to make it work.

Fast forward few months and not only we end up having random markForCheck() lines and injections of various singleton services everywhere, but also we don’t even know anymore, which component is dependant on what, and what data is changed where by which one. 😕

We reach a point in which our app has hundreds of components and tens of multiple singleton services, all of it being dependant on each other. We realise that we don’t know anymore what is dependant on what. We end up with an unsolvable maze of Front-end code.

The project gets so bloated with direct and non-direct dependencies between our components, that we get lost 😵, annoyed 😠 , until we finally give up and just switch back 🏃 to coding backends / jQuery / ”anything that was easier than frontend in 2018" again…

🤦‍ This is not how it should go.

While it is true that framework should help us to write our app to be easily scalable, the real truth is the framework had always meant to be just a basic scaffolding for our code. We can never rely only on the framework to keep our code in check for us. We need to do it ourselves.

Solution

Let’s think again about this “Smart” and “Dumb” split of components. The reason we get lost in our app is not because this pattern is wrong, but because very often we do it imperfectly.

My engineering plan for defining this pattern goes like this:

  1. Divide components into Smart and Dumb.
  2. Keep components as Dumb as possible.
  3. Decide when a component should be Smart instead of Dumb.

Let’s describe each of those points one by one.

P.S. In case you prefer browsing slides than a blog post, you can check out my presentation slides that I gave on a ng-poznan meetup under same title.

1. Divide components into Smart and Dumb

First, let’s define what Smart and Dumb components really are.

  • A Dumb Component is a component that works like a pure function.
    (A pure function is a function that for given function arguments, will always produce the same return value.)
    A Dumb Component is just like that. It is a component that for received data (inputs), will always look and behave the same, possibly also producing other data (events, via outputs).
Dumb Component
  • A Smart Component is a component that is more like an impure function.
    (An impure function is a function that touches “the outer world”: either by obtaining data from external services or by producing side effects.)
    A Smart Component is just like that. It is not only dependant on its’ inputs, but also on some kind of external data (“the outer world”), which is not passed directly via @Input(). It might also produce some side effects that are not emitted through the @Output() interface.
    For example, a component which gets current user data from a singleton service instantiated elsewhere; from an external API; or from LocalStorage. A component that changes the state of an external service; issues an API call; or changes the stored data in LocalStorage.
Smart Component

The Dumb component is sometimes called “Pure”, “Presentational” as well. The Smart component is sometimes called “Impure”, “Connected”, “Container”. Different definitions appear in the internet, depending on the author describing them or the framework on which he is focused in, but the whole concept of diving them in such a way is usually similar.

Keep in mind: Smart vs Dumb is not Stateful vs Stateless!
People often mistake those terms, but for me, they are completely unrelated to each other. See:

  • a Dumb Component has no external dependencies and causes no side effects (but still might or might not have a local state).
  • a Smart Component has external dependencies or causes side effects (but still might or might not have a local state).
  • a Stateless Component has no local state (but might still cause side effects).
  • a Stateful Component has a local state (but he doesn’t need to have any dependencies nor cause any side effects).

I’ve put a drawing to illustrate it more clearly, how those two divisions can be joined:

Smart/Dumb x Stateful/Stateless matrix

What is interesting is that solely on those drawings we can already get some serious conclusions:

  • The Dumb-Stateless component’s behaviour is the most easiest one to predict and understand.
  • The Dumb-Stateful component seems to be simple as well, because even though it has some local state, it is transparent to the others.
    From the others’ perspective, all that this component does, is still just receiving inputs and emitting outputs.
  • The Smart components seem to be the most difficult ones to grasp, because besides having inputs and outputs, they also are somehow connected with the outer world by obtaining data from it and/or causing side effects.

Now, my main point of this whole blog post is this: the Smart components are the worst evil. Because besides having clearly defined inputs and outputs, they also require some kind of external dependencies and produce some kind of side effects. Unfortunately, such stuff is always difficult to control in programming.

2. Keep components as Dumb as possible

Because of it, I say we should keep most of our components Dumb. By Dumb, I mean really dumb! I mean, a good Dumb component:

  • should not be dependant on external services — if it requires some data to work, it should be injected via @Input();
  • should not produce any side effects — if it needs to emit something, it should be emitted with @Output() instead;
  • should not mutate its’ inputs — because if it does, it actually produces a side effect that causes a change in the parent component’s data.
    A child should never directly edit parent’s data. If it needs to inform the parent that something had been changed, he should emit it as an event, which the parent should pick up and then properly act on it.

Code example:

3. Decide when a component should be Smart instead of Dumb

This point is hard. It made me rewrite the whole post a couple of times ;) The reason is because the clear distinction what should be Smart and what shouldn’t be is not that simple. But since I hate leaving computer science decisions up to the developer’s “personal taste”, finally I reached four conclusions:

3a. If it can be Dumb, make it Dumb

Think about what is the component’s role.

Is the role easy to predict and describe on paper? If yes, that means we don’t need to make him Smart. Inputs and Outputs should be easy to control his behaviour.

Examples:

  • a form control component that receives the current value and emits new values,
  • a form component that receives the initial form value and emits the new form values.

Basically, if there’s no reason not to make the component Dumb, then just make it Dumb.

3b. If multiple children are equally Smart, make them Dumb

For example, if you have a search view with multiple different filters that connect to the search page’s state, but all of them have the same role - to show the current filter’s value and possibly change it - then why repeat the same Smart logic in all of them?

Instead we could just have a smart FiltersListComponent that connects to the search page’s state and passes the filter values to the Dumb filter components beneath it.

Then, instead of having ten Smart components, we end up having one Smart one and ten Dumb ones.

3c. What cannot be Dumb, make it Smart

Eventually we’ll reach a point in which we can’t keep all of our components Dumb. At least one of them needs to be Smart. We need to keep the state of our app somewhere; we need to call the APIs from somewhere, right?

In most cases the best choice is to put it in the top view’s components.

Please note that doesn’t mean we need to put the logic directly into the component’s code though. It can be in a separate service like SearchPageControllerService. Or it can be in a Redux actions, state and reducer, if we’re using a redux-like structure.

All that matters isthat this Smart component will be in fact the only one that will have access to this external dependency and will be the only one emitting events to it.

All children of this Smart Component will be Dumb and respond to the world only with their inputs and outputs.

This will make easier to reason about who’s changing what and when in your view and what is dependant on what.

For example, in a typical search page only a SearchPageComponent should be Smart. All the other components in its’ view, like SearchPageResultsComponent, SearchPageFiltersComponent, SearchPageResultsItemComponent and so on, should be Dumb.

Search page example

3d. If the Smart one gets too big, divide it into separate Smarts.

“Make the top view component Smart” is a very good rule, but in some of the views, it might not be enough.

For example, a Gmail main inbox page has following features:

  • list and manage recent email threads
  • list and manage available folders
  • show online people from Hangout
  • allow to quickly write a new message

If we had only one Smart component on the Gmail page, it would need to take over all of its’ responsibilities. Quite a lot. There’s very high chance it would end up being a huge chunk of very complex code anyways.

So instead we could split it into a few Smart Components that have their own responsibilities:

  • ThreadsListView
  • FoldersListView
  • HangoutPeopleListView
  • NewMessageModalView

Each of those views would have only dumb children beneath it. So, it still will be manageable.

Frequently Asked Questions

1. How and when am I allowed to inject external services/dependencies? Can I depend on them, and how do I communicate with them?

Generally, you are never allowed to communicate with the outer world unless you are a Smart component.

As a Smart Component, you are the middle-man between the APIs & your app’s business logic and the other Dumb components.

Only a Smart Component should be able to communicate with external APIs by calling their functions and subscribing to their return values/promises/observables.

Whenever anything new comes from the API, you update your Smart Component’s state, which causes the change to propagate to its’ Dumb children.

Same goes with the other direction. Whenever your Dumb child wants to “change” anything, e.g. it wants to refresh the search results, it only emits an event. The Smart Component picks it up, calls the external API again, and only then propagates the change to its’ Dumb children by updating their inputs.

In practice it means only your Smart components located in the top of your component tree will be communicating with the external services. All the other ones located deep down in the tree will end up being Dumb and communicating with the others only through inputs and outputs.

However, there might still be cases in which it might be worth to connect your Dumb Component with the outer world directly. For example:

  • When clicking on a button in your ProductItemComponent, you might instantly navigate the app to a different URL, without the need of emitting and catching the @Output() productItemClick event.
  • When rendering your DateTimePickerComponent, you might get the current user’s timezone directly from some global variable.
  • When listing possible users in your UserSelectComponent, you might get the list of all online users directly from a globally available API service in your app.

Those three examples above are okayish to me, because even if they do have a connection with “the outer world”, it is only by getting a globally available and rarely being changed data in your application. But remember that it is still breaking the rules. If a situation arises where we want to have a different behaviour on ProductItemComponent#productItemClick, we want to use a different timezone for a specific instance of DateTimePickerComponent, or use a different selection of users in the UserSelectComponent, then IMO we should fall back on natural use of @Input() and @Output() interface.

2. How do I apply a change to my parent? Basically, how do I handle my app to change anything?

You emit an event and the parent catches it and handles the change internally. In effect, the components inputs and outputs form a kind of a cycle:

  1. Child B emits an @Output() event;
  2. Parent A catches the @Output() event and handles it either by:
    a) dispatching the event upwards — by emitting an another @Output() or
    b) handling the change internally — by updating its’ local state (and optionally telling the Angular to recheck its’ own and children’s bindings by ChangeDetectorRef#detectChanges());
  3. Child B receives the new data through @Input() , its’ ngOnChanges() callback is called and its’ view and children bindings are rechecked by Angular automatically.

P.S. Read more about the Angular Change Detection mechanism in my previous article: Mastering the Angular performance, part 1 — by dropping the magic of Change Detector.)

3. Can I ever mutate my inputs?

No. Mutating your input is a violation of a pure function — it actually produces a side effect which causes the data in your parent component to change without asking anybody to do so.

If you want your parent component to be changed, you should just emit an event with @Output() instead and handle the change in the parent component.

4. I need to get the current city weather in my CityListItemComponent. Can I just inject my tiny WeatherService in it and use it directly like this.cityWeather = WeatherService.getWeatherForCity(this.city)?

No, I think you rather shouldn’t. A CityListItemComponent sounds like a component that is nested deeply and which should be Dumb. If you make it dependant on an external service like WeatherService, it won’t be Dumb anymore. It will be more difficult to predict its’ behaviour, because an impure function is always more difficult to grasp than a pure one.

Instead, you could for example get the current weather for all the cities in the component that actually gets the cities and then pass it downwards using @Input().

5. I need to refresh the current weather for a given city with a refresh button located in my CityListItemComponent. Can I just inject my tiny WeatherService there and call WeatherService.refreshWeatherForCity(this.city) directly?

No, I think you rather shouldn’t. If the CityListItemComponent is a Dumb component, it shouldn’t produce any side effects.

Instead, emit a @Output() weatherRefresh event and handle it in the Smart ancestor which actually handles the weather data for the whole list.
This is because you want to keep your components’ code simple. The Dumb CityListItemComponent should only care about showing the city details and emitting UI events. It shouldn’t have any logic for fetching the current city’s weather in some API.

This whole separation is about having your components SOLID. The thing about getting and refreshing the weather for the cities in your city list page should be in only one place. Presumably, the list’s or the list page’s code (or its’ redux state and actions).

Pros & cons of following the Smart/Dumb split

Pros

  1. You can easily predict the Dumb Component’s behaviour.
    It is as simple as its’ public Inputs and Outputs interface.
    (Which, by the way, together with TypeScript typedefs, serves as an awesome documentation of your code.)
  2. You can easily test the Dumb Component’s behaviour.
    Testing a Dumb Component is as simple as:
    1. Define input values
    2. Instantiate Component
    3. Act on the Component (f.e. click it)
    4. Assert that a specific `@Output()` had been emitted.
    Testing a Smart Component usually requires much more than that: stubbing external dependencies, checking for side effects, etc.
  3. You can (quite) easily change the Dumb Component’s behaviour without breaking things.
    Whenever you change a Dumb Component:
    - Make sure the old interface is still working (or search & replace old usages of this Component which you can easily do, thanks to TypeScript)
    - The main behaviour of Component still works as intended.
    You don’t need to make sure that any external dependencies break this component, or if it produces some other side effects than before. It never did so and never will.
  4. The main logic of your app is controlled only by your Smart Components.
    No longer you need to read your whole code repository, just to see who’s fetching what where and what is changed what and where.
    Now you can see most of it mostly by looking at the HTML template of your Smart components.
  5. It is more performant.
    Because now you know what exactly depends on what, you don’t need anymore the NgZone nor the magical Change Detection mechanism that rechecks for changes of everything in everywhere. You can just skip NgZone and use ChangeDetectionStrategy.OnPush in all of your Dumb Components.
  6. It helps you avoid bugs.
    Less coupling of your code;
    Splitting it into smaller, more SOLID-like and pure bits;
    Avoiding side effects;
    Using typed Inputs and Outputs to transfer data throughout most of your app;
    - all of that decreases the complexity of your code and at the same time decreases the chance that bugs are going to happen in your code.
  7. Bonus: You can skip using ChangeDetectorRef#markForCheck() altogether.
    There’s just no need to use it at all.
    We don’t need to inform the Angular that something had been changed in the parent component, because as its’ child component, we are never directly changing it anymore.
    (If we do so, we do it through `@Output()` and it is the parent that handles the change itself.)

Cons

  1. You cannot inject dependencies wherever you want.
    You always need to think upfront what data will be required by your components and how it will be passed to them.
  2. You cannot mutate data passed through Input/Output.
    Instead, you use a local state or leave the data maintenance to Smart parents.

TL;DR:

  • Clearly divide your components into Smart and Dumb components.
  • Implement your Dumb components well.
    Never mutate @Input() values.
    Avoid depending on external services - use @Input() instead.
    Avoid causing side effects - use @Output() instead.
  • Avoid using hacky shortcuts in your code. In long-term perspective, they actually force you to spend more time on your code and are no shortcuts at all. They also make the whole app more difficult to understand, change and extend.

We started applying all those rules in our huge Recruitee’s Angular single page application a few months ago and we are already seeing huge advantages of it.

What do you think about all these rules? Do you enforce similar ones in your teams and projects? Maybe you disagree with something? Let us know in the comments.

Also, feel free to see my presentation, that I recently gave on the same subject. Maybe the slides will work better for you than a blog post.

P.S. I’m currently looking for a job! 🙂 If you’re in need of a full-stack web dev or a tech lead, check out my portfolio / CV and message me. Happy to relocate if needed.

--

--

Full-stack web developer (jtom.me) with over 15 years of experience. Past digital nomad and traveller. Now settled for good in Poznań, Poland.