Angular patterns 3: flexible and scalable design of complex pages
After an experience working with Angular on real life projects, I have developed my own set of patterns for organising scalable Angular code on large and complex apps. I thought it would be useful to share them.
I tried to separate them in small articles, and to be as concise and clear as possible.
Other articles in the series:
In my previous article I spoke about the benefits of removing “decision-making” responsibilities from components, in order to make them reusable. I said that components, who defer responsibilities to their parent, can be seen as “bricks” of your application, that you can place wherever you want, and make behave as you want.
It’s now time to look at the higher picture, and try to bundle together a set of such “bricks” in the root component of a page of your application.
At first sight, these are the problems that we need to solve, in order to achieve our goal:
Problem 1: I want to be able to instantiate all my brick components, and make them work, without ending up in a super-fat root component that does everything
In order to solve this problem, we need to group somehow brick components inside different “areas” of the page; each area should receive a different set of data as input, instantiate its own set of brick components, make them work, and send signals to other areas (or to the root component) in response to user interaction.
Problem 2: I want to be able to set up easily OnPush change detection strategy
The problem with “brick” components, i.e. reusable components that don’t decide anything about their internal status, is that it’s tricky to implement OnPush change detection on them, without in the same time binding them to the specific implementation of the parent component (hence, losing in reusability).
Why? Let’s look at an example.
The two components in the gist above behave very similarly, but they receive their inputs in a different way: the first component receives a bare number, while the second receives an object, that has a number among its properties.
The problem of OnPush change detection (which is its delight too, because it improves performance a lot) is that Angular checks objects by reference. If an object changes, but its reference doesn’t, Angular won’t realise that the child components needs to be updated. So, if you change the input number for the first component, the DOM gets updated, but if you do the same for the second component, the DOM will stay unchanged.
There are several workarounds for this problem, depending on what you want to achieve. One of them is using immutable objects (I’ll redirect you to this clear article).
The problem with all these workarounds is that, implementing one or another of them, you are somehow binding yourself to the specific context where the component is used. Hence, you lose something on reusability.
Let’s look at the example of immutable objects, for instance: if you go for that strategy, you have to hardcode the immutable object’s methods in your component.
This component won’t be reusable in contexts where the input is not immutable.
So, coming back to the list of problems we want to solve: implementing OnPush strategy without losing reusability of our “brick” components is definitely one of them.
Finally,
Problem 3: I want to be able to keep parts of the page isolated from the rest
Let’s say that we have a page that represents an article: the page is complex and contains a lot of different areas, there is a sidebar, a navigation, the article’s content, etc. All these components / groups of components are powered by the same “article” object that comes from the API. If you don’t implement OnPush change detection strategy, and you update, let’s say, the article’s title, the “article” object will be updated simultaneously in all the other parts of the DOM where it’s used. This is an example of a page which parts are not isolated.
Isolating components is not always the most important thing to do, it obviously depends on what you are trying to achieve. The problem is that Angular tends, for its design, to keep all your components up-to-date and synchronised with each other, so, if for any reason you need to do the opposite, and isolate them, you need to find a solution yourself.
Let me then rephrase problem 3: while I can find a workaround for each specific situation in which I want part of the page to be isolated, I’d prefer to have a uniform, standard way to enforce isolation, in the same way, on any given subset of the “brick” components I need.
So, these are the three main problems we have to face, when bundling together a lot of brick components into a page. Summarising:
- I have to develop a complex and highly interactive page, and I identified all the N reusable, “brick” components that I will use
- The “brick” components don’t change their internal status, they rely on the parent to do it. If the parent is directly the root component, and I write all the logic there, it will become huge and unmaintainable
- It’s difficult to implement OnPush strategy on single brick components, at least without affecting their reusability, so I want to be able to set up intermediate components between root component and “brick” components, where I can safely implement OnPush.
- I want to be able, in case of need, to isolate parts of the page, so that they don’t share the same models with the rest of the root component
My approach is usually to divide the page into different areas, and define a sort of controller component for each of them.
The controller component is responsible for
- instantiating all the brick components we need
- making them work depending on the context of the page
- implementing OnPush strategy
- interacting with the rest of the page
- enforcing isolation when needed
I personally use the word “controller”, because I personally think it describes correctly the concept of a component that controls other components; keep in mind that I am NOT talking about “controllers” in MVC pattern, nor “controllers” in old AngularJS.
So, let’s look at an example of a “controller” component: I’ll start from a very basic implementation, and progressively show we can push this pattern to solve all the three problems (code organisation, change detection, isolation).
The simple component above “controls” two brick components: ArticleHeader and ListOfContents.
Notice how I maintain the namespace system I defined a couple of articles ago: I added a “C” before the name of the controller component (the “C” stands obviously for “controller”).
The input is an article JSON that comes from the API. This JSON is decomposed, and only relevant parts of it are passed to the subcomponents. Remember that brick components, in order to be generic and reusable, shouldn’t have control over their internal status, this is why I call “splice” in this controller component.
We achieved a first degree of code organisation: instead of managing all brick components from the root component of the page, we do it from subcomponents, so our code looks cleaner and maintainable.
Let’s go a step further, and try to make this controller component interact with the rest of the page. Let’s imagine that in the sidebar there is a “publish” button, that turns the article into public, and has to fire different actions in the page (besides obviously pushing the value “public=true” to the API).
Now, to complete out controller component, we need some sort of communication system to let different controller components to send and receive signals.
A way to implement it would be sending a regular Angular event up to the root component, this way:
But, if we do this, we are making the root component responsible for receiving this signal and passing it down to the other controller components. We don’t want this, because we don’t want the root component to be responsible for too many things, and as a consequence become too big, and complex to debug.
Controller components need a way to pass signals to each other without necessarily involving the root component. The root component should be notified of an event only if some data has to be pushed to the API
The one below is the solution I personally ended up using for all my projects:
The “dispatcher service” is a service that contains reference of a RxJS Subject.
The same subject receives and forwards signals of different types. Components can inject the dispatcher service, pass to it the list of signal types they want to be fed, and receive back a filtered Observable they can subscribe to.
The dispatcher calls look really simple and intuitive, once we implement them in controller components:
This is the bit where the component listens for a specific signal, and reacts consequently:
We are telling the dispatcher to notify us every time it receives a signal of type “PUBLISH”.
And this is the place where we send our own signal to the dispatcher
It’s now time to inject the “DispatcherService” (otherwise the controller components wouldn’t be able to use it). We’ll inject it at level of root component, so it will be scoped exactly in that component, and it won’t accidentally send signals elsewhere.
Another benefit of injecting DispatcherService at level of component and not globally, is that this strategy allows us to inject, locally, also a whitelist of all possible signal types that are going to be used in that specific page: this way we don’t risk any hard-to-find bug caused by misspelling signal types.
By the way, I have to say that I generally find non-global injectors very useful. Angular documentation tends to warn against injecting accidentally a service on a Lazy Loaded module, which would make the service local instead of global; I personally use global services rarely, and I would rather warn against the opposite of what the docs say, i.e. turning accidentally a local service into global by injecting it into a module rather than into a component. But that’s probably just my personal way of thinking.
Ok, let’s take a breath now. What did we achieve so far, and how does it fit in the big picture of scalability?
- We collected all the reusable “brick” components we needed in our page
- We decided that all the components used in the sidebar could be grouped together under the same “controller” component; all the functionalities that manipulate the sidebar are now moved into that component, leaving the root component clean
- We created a dispatcher service that is used by our sidebar controller component to send signals; these signals are received by other controller components, that react consequently
- Our controller component receives signals from other controller components as well: out of all possible signals that the dispatcher can send, the component filters only the ones it needs
We now need to put change detection and isolation in the picture. Before doing that, however, I would like to do a tiny but important refactoring to the latest implementation of our controller component:
What I did is separating the two lines of code, that were previously inside “deleteContent”: all the controller does now is sending a signal to the dispatcher to say that an element was removed. Then, separately, in the constructor the component listens to the same signal, and deletes the content accordingly.
The model “article” doesn’t get modified directly, but rather upon reception of the signal.
- The brick component sends up an event saying that an element has been deleted
- The controller component sends a signal to the dispatcher
- The controller component listens for the same signal it has just dispatched, and modifies the model “article”
- The brick component updates itself accordingly in the change detection tick
Why is this beneficial? Because, as it happens for brick components, deferring responsibilities to somebody else makes our controller component more reusable.
Let’s imagine that later we need to modify our page, and we want to delete that same element, but this time as consequence of some event that occurs somewhere else in the page. Thanks to the refactoring we just did, this modification is as easy as a breeze: it’s enough to fire the same signal from outside, and our controller component will behave exactly as expected.
The structure we are designing is flexible, and scalable too, because we were smart enough not to hardcode the element deletion in the controller component.
Using this methodology allows us to build our application in modular blocks that don’t interfere with each other: each controller component simply interacts with the others by sending and receiving signals.
It’s now time to talk about OnPush change detection strategy and isolation. (again, by “isolation” I mean the decision of sharing or not sharing the same object in two different components).
In my opinion these two concepts are very closely related: if a component has an object as one of its inputs, this object is checked by its reference, and normal OnPush strategy won’t work. To make OnPush strategy work, you have to find a workaround which final effect is always to somehow clone that object in the recipient component, i.e. the component becomes isolated, because it doesn’t share anymore the same object with other components.
If a component uses correctly ChangeDetectionStrategy.OnPush, its input objects have to be cloned, hence the component is isolated. If on the other side the component doesn’t use OnPush, input objects don’t need to be cloned, and the component is not isolated.
So, in a nutshell, OnPush = isolated.
I have already mentioned that implementing OnPush strategy on brick components would affect their reusability. Now that we defined “controller components”, I believe that these are exactly the right place to implement OnPush.
Instead of detaching a brick component from the change detection tree, we’ll detach controller components, that are nothing else than groups of brick components. This way, the same brick component can be reused in both a controller component that uses OnPush, and another controller component that doesn’t.
Again, the keywords here are reusability and scalability.
Let’s look again at our previous example, let’s focus on its inputs, and let’s try to understand how we can successfully implement OnChange strategy.
In the gist above I focussed on the action that deletes one of the contents of the article, and only showed the logic that is related to change detection.
Let’s ask ourselves the most important question about change detection:
Upon which circumstances does the HTML of my reusable component change?
- First, it changes when the “article” input changes
- Second, it changes when the dispatcher sends the signal called “DELETE_CONTENT”
If we want to implement OnPush on this component, we need to find a way to pass both events as Input(), and bypass the problem that such Input() is going to be checked by reference (because “article” is an object and not a native variable).
Let’s start doing it for the first input: “article”.
Using immutable objects is not the only way to make OnPush work on objects: we’ll use a different, clever technique that consists into turning the input object into an observable.
I don’t remember which was the first article where I read about this technique, but I think this one explains it well.
Basically, from the root component of the page, instead of passing the article object as a regular input,
…we’ll turn it into a behaviour subject first, and then pass the behaviour subject to the child controller component.
This time, the input we pass to the controller component is a behaviour subject, not the article itself. In ngOnInit, we subscribe to route parameter changes as usual, but this time we add the following step
If it’s the first time we are receiving an article from the resolver, we have to create a behaviour subject (a subject that, upon subscription, sends the last value stored to the subscriber).
If it’s not the first time, and the router is refreshing the component without re-instantiating it, then we reuse the same behaviour subject and use it to stream the new article.
And this is the corresponding implementation in our old controller component:
When the root component pushes a new article via the behaviour subject, the input’s reference doesn’t change, and thus the controller component is not supposed to update: this is why we have to call “cdRef.markForCheck()”, that triggers manually change detection for the controller component.
Let’s look at an important detail of this implementation: the order in which root and controller components are initialised. Actually, one of the main reasons why this implementation works is that the subject we chose to use is a behaviour subject, that holds inside the latest value received and streams it to a new subscriber. This is the order of events that are occurring:
- Parent component is initialised, and “article$” contains the initial article
- Controller component is initialised, gets “article$” as input, and subscribes to it
- Thanks to the fact that “article$” is a Behavior subject, it didn’t throw away the Article object, and it can pass it to the controller component even though the subscription happens after the subject has been created
We did half of the work: OnPush strategy now works fine with the inputs that come from the root component. We still have to deal with the dispatcher events, we’ll do it next.
Let’s take a little break to reflect about what we just did: is it worth, in order to make OnPush work, to use observables instead of immutable objects?
I think so: using immutable objects to trigger change detection is fine if those objects are simple and nested at one level (you have to call methods like “parentObject.get(‘property’)”), but with complex objects with a lot of nesting the situation becomes complicated.
Usually, the complex apps I have worked on had to retrieve a bunch of nested data and metadata from the API, then the root component would decide what part of that data was required in each controller, detach it from the original data object, and pass it down to the controller. Doing this using observables is natural and easy, and you don’t have to worry about object references.
And of course, another point in favour of observables is that we’ll use them to pass dispatcher events as inputs (as you’ll see in the next paragraph): it’s easier if the whole communication between root and controller components is all handled with the same technique.
Ok, it’s time to work on the dispatcher events too. We’ll do something very similar, and pass them as inputs from root component to controller component.
If before the controller component subscribed directly to the dispatcher’s observable,
…we’ll now need to generate the same observable in the root component, and pass it as an input.
In the root component:
Look at line 19 of the gist: in the constructor call, we fetch an observable from the dispatcher.
Then line 9: we pass the observable to the controller component.
Let’s now finally look at the corresponding implementation of the controller component:
The call to “article.contents.splice”, that was previously defined in the constructor call, is now placed in the ngOnInit, as a reaction to signals received by the observable that the root component has passed.
And, of course, we need to call “cdRef.markForCheck”, because we are using OnPush strategy, and the observable’s reference doesn’t change.
The object “article” comes from the root component and it’s artificially turned into a Subject, while “deleteContent” has always been an observable. Regardless of their origin, they are treated the same way by the controller component: it simply subscribes to them in ngOnInit.
And our goal is reached! Our controller component is isolated and detached from change detection tree, and it gets updated EXACTLY when it needs to be.
Let’s look at a possible lifecycle of our controller component, to understand what different roles have the behaviour subject inputs (inputs that come from the API, via the root component) and the observable inputs (inputs that come from the dispatcher, that in turn is fired by other controller components).
- Root component gets the article object from the API, turns it into a behaviour subject; it’s also possible that the API payload is much bigger than the article itself (it can be a list of articles for instance), in which case the root component has to create the subject and pass to it only the article it needs: in any case, this doesn’t matter from the point of view of the controller component, that simply receives an observable that streams articles
- The controller component we developed receives the article, that is not the same object held by the root component, thus it’s isolated
- The other controller components that belong to our page interact with each other, and the ones subscribed to “DELETE_CONTENT” event will change the copy of the “article” object they hold.
- After several signals sent and received by the dispatcher, we’ll be in a situation in which all controller components that implemented OnPush and isolation have potentially different versions of the original article
- Now, if, in this situation, the router navigates to a different route that points to the same root component, and reuses the root component by pushing a new set of values from the API, the root component will push the new article object to the behaviour subject, and this new article will replace “article” everywhere, wiping away all changes that had been done on each specific instance of it.
Behaviour Subject Input = a RxJS wrapper for data that comes from the API = it changes when the router fetches new data as response of the user navigating away
Observable Input = observable that corresponds to dispatcher signals = it changes when other controller components push signals to each other
You might be wondering, what is a realistic use case of having several cloned instances of the same original “article” object (point 4 above)?
Well, for instance, if you are developing an article editor: you might want to have a sort of draft, where you can add and remove things, and also a preview tool that shows how the article looks like. The draft can correspond to a controller component that enforces isolation, so that the user can do what he wants in that part of the page without necessarily affecting the preview.
Conclusion
I know, this has been a long article, I hope it wasn’t too difficult to follow.
I tried to introduce a technique that I personally am very comfortable with, so the goal of the article was to communicate somehow this comfort that I feel.
Simply, doing things this way (brick reusable components + controller components + OnPush and isolation) makes me always feel in control of the application, and makes me feel that the most complex application can be easily decomposed into modular blocks that can be developed in any order.
Let’s imagine I have to develop a complex, interactive page:
- I divide it into brick components and define the signature for each of them (inputs + outputs)
- Then, I group them into controller components, and define which of them need to be isolated and which not
- For each controller component, I write down clearly the list of signals they have to send to the dispatcher, and the list of signals they have to receive
- For each controller component, I also write down the list of objects it has to receive from the API:
- Now I have the complete signature of controller components, which means the full list of signals the send and receive, plus the full list of objects they get from the API
- Finally, for each controller component I can draw a simple diagram of all inputs and outputs, and from that diagram I start developing it
- During the whole process of development, I rest assured of the fact that anything I do is modular, and most important, I can develop any controller component in any order, because whatever I do is very unlikely to break the existing code