This two part article focuses on implementing lightweight, reactive Angular components with MVP like presenters.
In the first part we will look how slow running unit tests has lead to new requirements for our component structure in one of our projects. We will next see how and why the solution, described in theory and practice, has been chosen.
In the second part, a simple example application shows the new structure in action and helps to highlight the most relevant parts. This will then enable us to assess, if our solution could fulfill the requirements and expectations set at the outset.
Before getting started, in case not already clear from the description/title, it should be mentioned that the article is more focused on the details of structuring/implementing a single Angular component. Therefor, it will probably not provide much benefit when looking for solutions on how to structure multiple components from a global/application point of view.
The pain - Slow running unit tests
As one of our last project grew larger, we faced the issue of an increasingly slow running unit test suite. Further investigations revealed our Angular component unit tests as one of the major reasons for the increasing unit test time. These seemed to be slow due to the required compile
step triggered by compileComponents
This is fine for testing template related functionalities like the state a certain html element is in but not for e.g. state related tests.
For a loading spinner, as an example, there are (at least) two categories that tests could be written for:
- A test validating that the spinner shows/hides when e.g. a
loading
flag is set totrue/false
(template/ui) - A unit test validating that the
loading
flag is in the right state e.g. when a backend call is pending or not (business logic)
The first test category requires the compilation step. For the second test it only adds delay which, in case there are a lot of these tests, can lead to a slow down of the complete test suite.
In our project we had a high ratio of component state related (category two) to template (category one) tests, even though core business logic has already been factored out to "helper" classes/services and the like. So for most test cases the template compilation was not required, making the problem even worse.
The ideas
Based on the problem description from above we'd like to perform the compilation process only for tests requiring a template. This could be achieved by:
- only writing Integration Tests for these functionalities (e.g. using Cypress)
- having Dedicated Tests skipping the compilation process
- Moving the functionality (and therefore the tests) out of the component
Integration Tests
Completely relying on integration tests can work. However, there is a possibility for these tests to quickly grow large/complex in case the tested functionality is more involved than a loading spinner.
In addition, multiple test scenarios could lead to the same result(s) e.g. the loading spinner being hidden. The test would need to perform additional checks like e.g. a side effect (could be a backend call) or another element being displayed (e.g. an error message). As we like to treat our integration tests as black box tests the latter was not really an option for us.
With all this additional complexity and, especially effort, comes an inherent risk, that not all use cases/code branches will be covered (things happen when the going gets tough...)
More importantly all integration tests suffer from the same issue of not being as fast as unit tests (probably even slower than Angular component tests) rendering these invalid for solving our problem.
Dedicated test suite/test file
Why triggering compilation for tests not querying the template?
An option would be to move the compilation step out of the beforeEach
block into a dedicated method which is only called when the template is required for testing.
Another alternative could be to have these tests in a separate test file which does not compile the template and directly calls the component constructor (similar how Services
or Pipes
are tested).
The proposed solution avoids the overhead created by the compilation process. In case needed the customized test file generation could be simplified by writing a schematic.
Moving functionalities
The approach is similar to moving the tests into a dedicated file. It takes the idea even further by „separating“ the complete functionality from the component itself and moving it to a dedicated Service
or JavaScript Module
.
As this new service/module would not have any template, the issue of compilation would be gone.
As an additional benefit, moving the implementation out of the component makes it more lightweight. In case free of core business logic by means of state management solutions (being it „simple“ Services
or a dedicated library like NgRx
) the component only contains view related properties (like the isLoading
flag for the described loading spinner example).
For that additional benefit the option looked most appealing and was chosen for our project. We not only figured that it can solve our initial problem (slow running unit tests) but also be an opportunity to bring more structure to the components and application.
The new structure
Components should already be lightweight/free of business logic in case a proper state management solution is in use. Nevertheless, we have experienced that, despite using NgRx
for dealing with global state, the orchestration as well as the required component related implementations can grow quite substantial for some components. Also, not every state (at least for us) is supposed to be global state and putting all that (transient) state into the component lead to our testing and structure issues in the first place.
For that reason we were looking for a solution filling the gap between managing global state and more complex local state/business logic (maybe even shared between multiple components).
So we were looking for an approach that:
- reduces our unit test time for Angular components
- creates more lightweight components
- improves encapsulation and modularization for components
- enables sharing parts of the logic between component siblings and/or descendant if and only if it makes sense
Having settled on the idea of factoring out logic and state from our components (as described in the previous part) we iterated a few times until we reached our current structure. In hindsight, our final solution was inspired by a combination of the Flutter BLoc- and MVP pattern.
The Flutter BLoc pattern
At the time I had been investigating Flutter as an option/replacement for our non native mobile solutions (in case requested by clients). The BLoc pattern is one of the available (and popular) options for managing (global) state in Flutter. As it is not required for this article to deeply go into the implementation details here is my short summary (no claim to completeness):
Business logic components are a lightweight approach for managing state in a reactive/-event driven fashion. The reactivity within the bloc pattern is achieved by using Streams
or Observables
. Some implementations introduce the notion of Events/Actions
(similar to Redux
) triggering effects and/or state changes. (more details can be found e.g. here).
In my Flutter applications I used it for global state management. However, I had (some) of the same issues with Flutter widgets (widgets are similar to components) as discussed in the previous section:
- testing widgets is more involved and slower (although faster then Angular component tests)
- widgets can grow complex in regard to state and business logic
For the Flutter applications, I somehow solved it by using "BLocs" for local state as well. So each widget, with enough complexity justifying it, is associated with its own BLoc containing the state and business logic (provided either by prop passing or InheritedWidgets
).
I should mention however that I have always kept my BLocs simple instead of implementing these "by the book": So plain old classes which expose state as streams and updates are trigger by simple functions calls on these BLocs (so no notion of events and the like), keeping the overhead pretty low.
It served me well in regard to solving the issues for my Flutter applications. What I particularly liked about the approach was the reactivity it provided for the presentational layer in regard to state updates, similar to what we get from NgRx
for global state in Angular.
So inspired by that we moved all the component related business logic into an associated service. As we are using NgRx
, core business logic was already been taken care of. In hindsight, what we came up with in the end is pretty close to presenters from the MVP pattern.
The MVP pattern
Initially we named the new services classes ComponentBlocs
. However, I wasn't really satisfied with this term because:
- our component "BLocs" never implemented the interface described by most BLoc related articles/libraries (e.g. we had no notion of
Events
) - we are not managing global state or core business logic in these classes
- it somehow "felt" wrong ;)
Later, when (coincidentally) watching the introductory talk by Lars Gyrup Brink Nielsen about Model-View-Presenter
in Angular, I saw a similar structure and idea in there (at least I think so). It is not exactly the same to what we came up with e.g. we do not always have presentational components. However, it is close enough so that MVP and especially Presenter
seems to be a good fit for our component associated services.
What is/was important to us (brought over by the BLoc pattern) is, that it should enable our components to react to state and state updates managed by the presenters. This is especially the case when used in combination with NgRx
as it then, due to both being based on reactive principles, allows for a seamless integration of global and local state.
Today I like to use the term Reactive Presenter
although this may not be exactly true since it is not only the presenter being reactive but also it's clients (usually Angular components).
As reactive
is a somehow loaded term and can mean different things for different people, I will stick with just Presenter
for the remainder of the article. The important point I want to pass here is, that our presenters should enable reactivity, both for itself and its clients.
As we now figured that our new component structure closely leans on the ideas of presenters in MVP, we need to answer the questions:
- What is a presenter
- How can it be made reactive in Angular
What it is
The are already lots of resources out there describing the MVP pattern in general e.g. the Wikipedia, including presenters. However, it does not seem to be too popular in the Angular realm (at least this was/is my impression).
As mentioned, the talk and article by Lars Gyrup Brink Nielsen make for a good starting point.
How it is used
Presenters are implemented as Angular services/injectables and associated with the component using component providers
. This keeps the services and their states scoped to the instance of the component instead of being globally available like Services
e.g. provided in root
. Limiting the scope of presenters also binds their lifecycles to the providing component, coming in handy when having to perform e.g. clean up logic onDestroy
. It also nicely separates the states (in case there is any), so that multiple instances of the same component cannot interfere with one another.
A component can have multiple presenters allowing state and state related logic being separated into different presenters, enforcing encapsulation.
On the contrary, a single presenter can be injected into multiple components, either for reusing business logic or sharing state. The latter can avoid prop drilling by injecting the top level presenter into a "leaf" component (similar e.g. React Context
can be used).
To add support for reactive state updates (not a requirement in general) our presenters are completely based on Observables. This not only allows for declarative state orchestration (global and/or local) but also a seamless integration with other reactive Angular features like the RoutingModule
or state management solutions like NgRx
. As an additional benefit, it can give us some advantage in regard to change detection, which we will discuss later.
To make the described setup more tangible we will now look at an example implementation in the second part of the article.
Top comments (0)