You’re trying to code declaratively and then you come across a situation like this. You want to use some kind of 3rd party API, like this alert controller here, but its API is imperative.
In this example, I have an alert I want to display every time this newPlayers
stream emits a value.
This article is adapted from the following video:
The Imperative Dilemma
You can see that happening in the image below. Every 10 seconds or so we have this alert pop up to say a new player has arrived.
I’m trying to code declaratively, but since this API I am using is imperative I need to do this manual step of imperatively creating a new alert and displaying it.
Since I want to display data from the newPlayers
stream, I also have to do this manual subscription to pull that data out and pass it to the imperative API - again, breaking out of that beautiful reactive/declarative paradigm and creating a manual subscription that needs to be handled and unsubscribed from appropriately.
Turning Imperative APIs into Declarative
So, are we just out of luck here? The library we are using is just imperative so we’ll have to suck it up and deal with it?
Of course not. There’s a reasonably simple trick we can do to turn an imperative API like this into a declarative one.
The idea is that we just take the imperative functionality and wrap it up inside of our own little declarative component. Let me show you.
@Component({
selector: 'app-home',
template: `
<ion-header>
<ion-toolbar>
<ion-title> The Game </ion-title>
</ion-toolbar>
</ion-header>
<ion-content class="ion-padding">
<p>You just lost</p>
<app-notification-alert
*ngIf="playersService.newPlayers$ | async as newPlayer"
message="A new challenger '{{ newPlayer.name }}' appears. Player ID: {{
newPlayer.id
}}"
></app-notification-alert>
</ion-content>
`,
changeDetection: ChangeDetectionStrategy.OnPush
})
Now we are looking at the declarative example in the image above. As you can see, we now have this custom component that accepts a message input for the alert we want to display.
This allows us to just use the async pipe to subscribe to our newPlayers
stream for us and no manual imperative step is required now to create and display the alert. Whenever this message input changes, for example, when the stream emits a new value, it will display a new alert. Again, you can see that happening in the image below. If we wait a few seconds, we see that first alert being triggered. It works exactly the same as it did before, we just have this different style now.
To achieve this, I’ve created this separate directive here called Notification Alert Directive.
I’ve given it a selector of app-notification-alert
which is what allows us to drop it in the template like this:
<app-notification-alert
*ngIf="playersService.newPlayers$ | async as newPlayer"
message="A new challenger '{{ newPlayer.name }}' appears. Player ID: {{ newPlayer.id }}"
></app-notification-alert>
It’s a directive not a component because it doesn’t need a template of its own. All we have in here is a ngOnChanges
that listens for changes to the message input and creates the alert for us using the same imperative API from before.
Evaluating the Trade-offs
Now, you might be thinking, this is just imperative programming with extra steps - we’re still using the same imperative API but we had to build this whole extra directive on top of it. The only obvious benefit we are getting here is that now we can use the async pipe instead of that manual subscription.
If this is what you are thinking then, you’re basically correct. But all declarative programming is like this, it’s just a higher-level abstraction on top of imperative code. It’s just that this time we are writing the abstraction ourselves since the API we are trying to use is not already declarative.
In this case, we are putting in a bit of extra work to keep our core codebase declarative. In some cases, even if you are generally coding declaratively, you might decide that there isn’t much value in creating a declarative wrapper and sometimes you might just want to use the imperative API directly and be done with it.
Conclusion
I’m not going to tell you you’re wrong, because I am a bit of a declarative code zealot, but personally, I like having hard rules that remove decisions - like making the entire codebase declarative as a rule, even where it might not technically be required or even useful. It avoids having to make these kinds of decisions and helps to prevent those “well this is a special case” decisions from creeping into places they really shouldn’t.
For more information on declarative code, feel free to check out this video here.