In this blog post, we’ll discuss the balance between declarative and imperative code in Angular. While I often advocate for the benefits of declarative code, you may have noticed that the code I write is not “fully declarative”. In fact, in the Angular space, it’s rare to find anyone who codes “fully declarative”. I’m not even sure that’s entirely possible.
This article is based on the following video:
Why Isn’t My Code Fully Declarative?
Usually, the reason the code I write is not fully declarative is because I often do this:
I next
subjects. Can you believe that? The problem with next
ing subjects or setting signals, and why it is imperative not declarative, is because we don’t know what this thing is or how it changes over time just by looking at its declaration.
To understand what this is, I need to go searching through this component. I can then see that this subject is being nexted
in response to the next and previous page buttons being clicked.
Despite this, I’m still composing a stream that is created by next
ing this subject into my stream of data. Overall, this is quite a declarative approach, with just a tiny bit of “cheating”.
Going Full Declarative
However, we don’t need to “cheat” like this. We could go full declarative mode. We don’t need to create a subject that we next
in response to buttons being clicked because we can just compose the DOM events from those button clicks directly.
So, we end up with something like this:
We can delete the subject and the handlers we were using to next
it, and now we just have this one declaration that defines the behavior of what the currentPage
should be.
We create observables using fromEvent
for our next and prev buttons in the template. To make my life a bit easier, I’m mapping these to 1 and -1 to indicate how they should affect the current page number.
The scan
operator is similar to a reduce and basically allows us to collect whatever values have ever been emitted on this stream. We start with an initial value of 1, and then each time either of our buttons are clicked we add it to the initial value.
So, if next
was clicked it will add one, and if prev
was clicked it will subtract one. We also add this check to make sure we don’t go below 0.
You might also notice that we start with this timer()
observable stream and then immediately switch away from it. This is a nice little hack to deal with the fact that the nextPageButton
and prevPageButton
aren’t defined immediately - this delays the evaluation of our buttons until after the template has been initialised.
Expanding This Strategy
But what if we try to expand this strategy to the rest of the application? What if we have a service that is sharing data throughout the application - and we want to be able to modify that data throughout the application - basic CRUD operation stuff.
As an example, we will use this service from the example signals application I created for a previous video. This one doesn’t next
subjects, it sets signals, but it’s the same idea.
We have this add
method that can be called in the service, and calling that method will modify this signal, and that is where all the data we want to share throughout the app is stored.
But we know we can’t next
subjects or set signals because that would be naughty. We might try to do the same thing again here - rather than imperatively setting a signal or subject by calling the add method, we could just use the event directly from wherever the add action is originating, just like we did with the pagination example.
The problem here is that the action is going to be triggered from within a component, or it might be triggered from multiple different components, and we want that stream in our service.
So our service would need to do something like get references to every component in the application that wants to trigger a change and compose a stream of whatever is triggering those changes. But those components might not even exist yet.
The Imperative Detour
At this point, we give up on our dreams of having a fully declarative codebase and just stick with our simple, but imperative, add
method that nexts
a subject or sets a signal.
This is a nice pattern in my opinion, and it is only a very minor concession. Although technically it is imperative, it’s not like we are giving up on this whole declarative idea. Next
ing a subject or setting a signal in this case is like this little imperative detour that makes things easier and then we get right back onto the declarative highway.
So, we establish that a little bit of imperative code can be OK - even required in some cases. Now that we know that, do we still keep this fully declarative pagination approach at the component level just because we can in this case?
Maybe. That’s up to you to decide. In most cases, I prefer to go with the little imperative shortcut. I think it simplifies the mental model a great deal. But I think both approaches are fine, and I might even end up changing my mind on the subject.
If I could make my code 100% declarative I probably would - even if it were harder in some cases, but since I can’t, I feel like I’ve got a bit more freedom in determining where I should draw the imperative line.