Today we’ll see how to organize our components so they are small, reusable, performant and only contain the necessary amount of logic. Let’s go through these bullet points.
Generally we want to solve a problem with the least amount of code that still can be understood. We don’t want to use hacks and tricks that are hard to figure out, but we want to make it as terse as possible. That’s the whole point of dividing our application to smaller components.
We’ll write our components with the Single Responsibility Principle in mind – a component will only have one responsibility, and one reason to change.
The Single Responsibility Principle (SRP) is a part of the SOLID principles. These principles are a set of best practices writing object-oriented applications. The SRP states that a class should encapsulate one and only one responsibility of the application, and that responsibility should be defined entirely in the class.
We touched this subject when working with the SharedModule. A set of our components will eventually be reused in multiple places of our application. We need to make these components as dumb as possible – by removing any business logic from them, and by having a well-defined interface.
Our application needs to be responsive and fast. We’ll minimize the need for change detection and will have clear cases when it should be run on our components.
There are two kinds of components. In React, they are called container and component. Angular has the same concept, but here they are called smart and dumb component, respectively.
A container or smart component is concerned with the how. It inject services, fetches data and passes it to its host of dumb components. Here’s the code and template of a smart component:
Notice the following:
- Our component injects a service.
- It also defines an Observable. Dumb components generally don’t define Observables. They deal with eagerly-available data.
- It has an ngOnInit hook, where we do some data fetching.
- Its template instantiates our dumb component (app-repo).
A dumb component’s task is to display data. It does not inject services. It only contains logic needed for displaying items (e.g. toggling an accordion, maintaining the currently selected item). A dumb component only communicates with its parent component, by using @Input and @Output properties.
They are commonly defined in the SharedModule. We do this to maximize reusability. It’s a great practice to start early on. I ran into this issue countless times when wanted to reuse a component in another module, only to find out that I’m causing circular dependencies.
Let’s see the code and template of a dumb component:
- A dumb component is dumb. It only has an @Input property to accept data from outside.
- The input parameter is not an Observable. You’d open up yourself to serious subscription-management nightmares if you pass Observables to dumb components.
- I prefer using strongly-typed parameters, the Repo class represents a GitHub repository. It’s a good practice to get into, since you’ll get compile-time warnings and errors.
- The main task of the dumb component is to display data.
- We marked it with OnPush change detection. We do the same with smart components. We get into change detection a bit later.
- Write your smart components to fetch data and handle user communication (form submission, button clicks, etc.).
- Place your smart components in the feature module where they are needed.
- Write dumb components to display data. Do not put business logic into them.
- Place dumb components in the SharedModule to avoid circular dependencies.
- Dumb components can be promoted and smart components demoted. When this happens, move the component to its new place.
- Litmus test: if you have a constructor your component is probably smart.