The article discusses advanced techniques for component communication in Angular applications, focusing on the use of hierarchical dependency injection and providers to manage state and avoid complex input/output patterns.
Abstract
The second part of a series on Angular providers, this article delves into enhancing component communication within a music library application. It demonstrates how to use Angular's hierarchical dependency injection to share state between components without relying on traditional input/output data flow. By implementing a factory provider and an InjectionToken, the author illustrates how to pass a simple value like a Subject for a Song object through the component tree, allowing for a more streamlined and maintainable codebase. The article also addresses compatibility with different state management solutions, the reusability of components, and the integration with Angular's OnPush change detection strategy to ensure optimal performance. The approach simplifies the API of components and reduces the need for verbose wiring of inputs and outputs, especially in cases with multiple component layers or when using projected children.
Opinions
The author advocates for using Angular's hierarchical dependency injection as a superior alternative to the conventional use of inputs and outputs for component communication, particularly for complex component trees.
The article suggests that relying on a simple Subject or Observable as a provider is a lightweight and efficient way to share state without the overhead of full provider classes or
Making full use of Angular providers — Part 2
If you haven’t read part 1 of this series, I recommend doing so before going further, as some concepts we’re going to use here have already been introduced. In this second part, we’ll look at how providers can improve communication between components by implementing a very basic music library component.
Our starting example
The music library will consist of songs grouped in albums and organized by artist, according to the following interfaces:
Our music player will then be composed of 4 very simple components:
PlayerComponent, which receives the entire library and displays one ArtistComponent per artist.
ArtistComponent, which displays the artist’s name and one AlbumComponent per album of that artist.
AlbumComponent, which displays the album’s name and one SongComponent per song in that album.
SongComponent, which displays the song’s name and is clickable to start playing the song.
Here is a live example of our music player, without the playing capabilities implemented yet:
The song component, all the way to the bottom, is the one that will start playing a song on click (obviously to keep this example simple, “playing” will just be visual and not actually play the song). So how can we add this playing functionality to the song component?
Using inputs and outputs
A common pattern for this behavior is to pass inputs from the player down to the song components, and outputs back up to react to a click. This has a lot of great properties: components are “pure”, data flow goes one way and OnPush change detection strategy works out of the box. It is the definition of “lifting the state up”, which is a trendy catch phrase in UI dev these days.
So let’s implement this in our example by adding inputs and outputs to our various components in order to track the currently playing song:
This implementation works as expected: clicking on a song “plays” it, and clicking on another interrupts the previously playing one and starts playing the new song.
However, we notice immediately that components that don’t care at all about the currently playing song suddenly need an extra input, an extra output and two bindings in their template:
We only have three layers and one property here. In a real-life application or on more complex components, we would have to multiply these inputs and outputs on many more components. Suddenly we end up with a (flying) spaghetti monster of inextricable code.
Providers to the rescue!
Thanks to dependency injection in Angular being hierarchical, we can declare a provider in the PlayerComponent and inject it directly into our SongComponent, entirely bypassing the intermediate layers. That’s what many state management libraries like @ngrx/store do to pass the state around the application, but for our purpose it would be overkill to resort to them. Even worse, if our music library component was written to be reused in different applications, not all of them would necessarily use the same state management solution, so we couldn’t just pick one and neglect compatibility with the others.
At this point, your reaction might be: “But I don’t want to declare a full provider class for a simple value, that’s still way too complicated!” That’s a fair point, but luckily we don’t have to declare a full class.
The object we’re sharing is a plain RxJS Subject for a Song value, so we can assign a new playing song and react to changes in this value by subscribing to it. A naive solution would be to use a value provider, but doing so would result in all the player components to share the same instance of the Subject, if we were to have more than one on the same page.
Now the SongComponent can inject it with our new InjectionToken:
Here is a live example to see it work all together in action:
As you can see, the ArtistComponent and AlbumComponent don’t have any code related to the currently playing song anymore, all the SongComponent instances share the same subject to keep at most one playing song at all times.
A cool side effect
As I mentioned at the end of part 1 of this series, an interesting side-effect of this pattern is that it allows implicit communication between a parent and its projected children as opposed to explicit communication through inputs and outputs.
Imagine we couldn’t assume the structure of the library, so we’d have to let the app itself iterate over the artists, albums and song to use the individual components that form our player. Something like this:
Our components would still provide a nice view for each artist, album and song, and would still be in charge of playing a song when it’s clicked. If we were to use inputs and outputs the same way we did at the beginning of this article, the app itself would be in charge of wiring these inputs and outputs. Worse, our components would become less reusable because everyone using them would now have to handle that seemingly internal wiring themselves.
Because dependency injection is hierarchical (children of a component can inject its providers, even if they’re projected children), our solution with providers works exactly the same:
As you can see, this is a fantastic way to implement communication between public components while keeping their API as simple as possible.
But my project has a strict policy of using OnPush change detection!
Well, good for you! That’s a great policy to guarantee good performance of an app, and I’m glad you can enforce it across your project. Maybe you tried what we just saw, and were disappointed to see it didn’t work out of the box?
To echo what we mentioned earlier, the inputs and outputs solution has the advantage of being “pure”, which means Angular knows change detection has to be triggered if and only if inputs have changed (or events were fired in the view of that component, but we can ignore that for now). But it’s very easy to make Observable patterns work with OnPush change detection strategy using ChangeDetectorRef.markForCheck(). In our case, we just need to make sure that a SongComponent marks itself dirty when it goes from playing to not and vice-versa:
And here is our example working fine with every single component using OnPush change detection strategy:
All we had to do was to add 3 simple lines to our SongComponent and we still get the benefit of not having to write inputs and outputs on every single layer.
I hope this gives you even more reasons to use providers to communicate between components, even if all you need to share is a simple variable. Declaring a Subject or any other kind of Observable as a provider creates no overhead, and can drastically reduce the amount of code you need in real-life applications with many layers.
In the next part of this series, we will look at more advanced cases of component communication, for instance in recursive components. As usual, thanks for reading and questions are welcome in the comments!