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 oneArtistComponent
per artist.ArtistComponent
, which displays the artist’s name and oneAlbumComponent
per album of that artist.AlbumComponent
, which displays the album’s name and oneSongComponent
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.
Remember how we used a factory provider in the previous episode of this series? We can do the same again, and because our shared object is so simple we will simply declare the factory inline:
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!