DEV Community

Cover image for Wrapping Imperative APIs in Angular
Mike Pearson for This is Angular

Posted on • Edited on

Wrapping Imperative APIs in Angular

This series explores how we can keep code declarative as we adapt features to progressively higher levels of complexity.


divider

Progressive Reactivity Rule #3

Wrap imperative APIs with declarative ones.

Imperative APIs are better than no APIs, and they tend to precede declarative APIs. Why is that, and what can we do about it?

Declarative code is more comprehensible than imperative code, as you saw in the example in the first article in this series. But in order to write comprehensible code, you have to comprehend what you're writing. For example, it's easy to declare a variable with a bad name, but writing a comprehensible name requires a comprehension of what that variable represents.

When developers solve difficult or novel problems (like creating a new framework) they lean towards an imperative style of programming, because it is easier and they are used to thinking imperatively. The imperative APIs cause applications that use them to become more imperative as well, which then grow into incomprehensible balls of spaghetti code. Inevitably the community creates declarative wrappers for the APIs, and then finally the APIs themselves are changed into something more declarative.

So, we should not be surprised or upset that Angular has plenty of imperative APIs. AngularJS was an early SPA framework, and was solving difficult and novel problems. In fact, AngularJS brought reactivity to DOM updates with change detection, and it was that very mechanism that created the problems that ended up being solved with the imperative APIs. And then Angular tried to maintain some continuity with AngularJS, so it inherited much of that imperative style.

Angular is unfairly disregarded by many developers who moved to React or another framework (yes, framework) after AngularJS, and have no actual clue about what modern Angular looks like. However, other modern frameworks have made progress that Angular has not been able to make. Although they are largely ignorant of the benefits of RxJS, they do have more many more declarative APIs than Angular, and that sometimes make me jealous.

Modals

My favorite example is modals. In the Angular ecosystem, it seems a given that you have to open dialogs with an imperative .open() command. But it doesn't have to be this way. Literally every other component library in literally every other modern front-end framework has declarative dialogs that react to state, instead of depending on out-of-context imperative commands to open them. Don't believe me? Well, even if you do, I want to actually show you. Let's look at Vue, React, Svelte, Preact, Ember, Lit, Alpine and SolidJS. Feel free to skip to Angular. It's a long list.

Vue.js

Top Vue Component Libraries

Vuetify

Vue—Vuetify Dialogs

Quasar

Vue—Quasar Dialogs

Bootstrap Vue

Vue—Bootstrap Vue Modals

React

Top React Component Libraries

Material UI

React—MUI Dialogs

Ant Design

React—Ant Design Modals

React Bootstrap

React—React Bootsrap Modals

Svelte

Top Svelte Component Libraries

Svelte Material UI

Svelte—Svelte Material UI Dialogs

SvelteStrap

Svelte—SvelteStrap Modals

Smelte

Svelte—Smelte Modals

Preact

It was hard finding component libraries for Preact, to be honest. I've included the only one I found with documentation that was easy to find.

Preact Material

Preact—Preact Material Modals

I believe simply rendering the Dialog element opens it, so that is declarative.

Ember

Top Ember Component Libraries

Ember Paper

Ember—Ember Paper Dialog

Ember Frontile

Ember—Frontile Modal

SL Ember Components

Ember—SL Ember Components Modal

Lit

Lit is for creating web components, so I will just look at web component libraries for this one.

PolymerElements Paper Dialog

PolymerElements Paper Dialog

Vaadin Web Components

Vaadin Web Components Dialog

Wired Elements

Wired Elements Dialog

Alpine

I only found this example:

Alpine Dialog 2

SolidJS

SolidJS is an amazing library, but it is still very new. I couldn't find many component libraries with dialogs. But there is this example on SolidJS's own website, and it shows a modal being opened declaratively. I guarantee that any component library that pops up for SolidJS will be declarative like this.

SolidJS Dialog

I did find this unofficial component library for Headless UI:

unofficial component library for Headless UI—Dialog

Angular

Finally, Angular. Top Angular Component Libraries

Angular Material

Ah, Angular Material, the official component library for Angular. Let's see how to use dialogs:

Angular Material—Dialog HTML

Okay, so it's calling a method. That's breaks our Rule 2. What does that method do?

Angular Material—Dialog TypeScript

This is the first component library out of the 20+ for 7+ frameworks I've seen that opens dialogs imperatively.

The 2nd and 3rd libraries are also imperative.

ngx-bootstrap

Angular—ngx-bootstrap Modal

ng-bootstrap

Angular—ng-bootstrap Modal


To summarize,

Framework Library 1 Library 2 Library 3
Vue ✅ Declarative ✅ Declarative ✅ Declarative
React ✅ Declarative ✅ Declarative ✅ Declarative
Svelte ✅ Declarative ✅ Declarative ✅ Declarative
Preact ✅ Declarative ✅ Declarative ✅ Declarative
Ember ✅ Declarative ✅ Declarative ✅ Declarative
Lit ✅ Declarative ✅ Declarative ✅ Declarative
SolidJS ✅ Declarative ✅ Declarative ---
Alpine ✅ Declarative --- ---
Angular ❌ Imperative ❌ Imperative ❌ Imperative

But you don't have to suffer.

Again, we should not be surprised or upset that Angular has plenty of imperative APIs. AngularJS was an early SPA framework, and was solving difficult and novel problems.

But guess what else? The Angular team is not the pope. You can have an opinion, even if it goes against what the community assumes is correct because it is the default solution handed down from the beloved Angular team.

So I created a wrapper for Angular Material's dialog component that you can use like this:

<app-dialog 
  [component]="AnyComponent" 
  [open]="open$ | async"
></app-dialog>
Enter fullscreen mode Exit fullscreen mode

GO TO THAT GIST AND COPY IT INTO YOUR CODEBASE RIGHT NOW.

Stop living in pain. Enjoy declarative dialogs.

You should be proactive and wrap ALL imperative APIs in declarative APIs.

Other Imperative APIs in Angular

Dialogs are not the only place Angular has imperative APIs. We still have to write imperative code for component lifecycle hooks. Angular Reactive Forms should be called Angular Imperative Forms. There are others, too. I've written in the past about how to deal with these other imperative Angular APIs. Careful, it's a premium Medium article. Here's the link.

Side-Effects

Side-effects do not have to be be imperative. The entire DOM is technically a side-effect, but in Angular we (usually) write declarative templates for UI state. So why can't we handle all side-effects declaratively?

Dialogs are examples of APIs that end up outputting something to the user, but what about more behind-the-scenes APIs like localStorage?

For localStorage, reading state can be done synchronously, so that's not an issue when initializing state. The problem is when we need to push data into it because it has to be done imperatively with localStorage.setItem().

Rather than calling setItem in a callback function, we wish localStorage itself could declare its own state over time. Something like this would be nice:

this.localStorageService.connect('key', this.state$);
Enter fullscreen mode Exit fullscreen mode

But what subscribes? What unsubscribes? And what if state$ chains off of an http$ observable? Do we want to trigger it immediately by subscribing? Clearly local storage should not be a primary subscriber to what it's watching. But RxJS does not support "secondary" subscribers, or passive listening of any kind. So, I see 2 possible solutions:

  1. Tack on a tap to state$'s declaration. So everything that subscribes to

    state$ = defineStateSomehow().pipe(
      tap(s => localStorage.setItem('s', JSON.stringify(s))),
    );
    

automatically triggers our callback function every time state$ updates (if it has subscribers).

  1. Create a wrapper component like we did for dialogs, so we can use it like this:

    <app-local-storage
      key="key"
      [item]="state$ | async"
    ></app-local-storage>
    

    Is this weird? It kind of is. But it's so convenient. And if we want we can wrap that element in an *ngIf that controls when app-local-storage subscribes.

My thoughts are evolving on this, but #1 is still imperative, with that callback function passed into tap(). So I would personally prefer #2. But it might be a syntactic dead end that we'd have to undo if we encountered an unexpected scenario that needed more flexibility.

Other imperative APIs can return observables, so they can be expressed reactively much more easily. For example, a POST request can be done like this:

submit$ = new Subject<void>();

submissionSuccessful$ = this.submit$.pipe(
  withLatestFrom(this.form.valueChanges),
  concatMap(([, data]) => this.apiService.submit(data)),
);
Enter fullscreen mode Exit fullscreen mode

Most of you are probably used to having a submit method instead. But that is imperative when it could be reactive. Why do you think $http.post returns an observable? Because POST requests return values, and it's not just so they can be lost in the depths of our app. We should probably have a wrapper for a toast component so we can show the user that their submission was successful:

<app-toast
  [message]="submissionSuccessful$ | async"
  duration="3000"
></app-toast>
Enter fullscreen mode Exit fullscreen mode

This is really nice. Hopefully Angular component libraries start providing declarative APIs for all of their components.

Summary

Imperative APIs are better than no APIs. We are grateful for developers who work on the difficult problems frameworks are solving. We are not surprised that the first APIs that solve problems turn out to be imperative.

But we want to code declaratively. So, when we encounter an imperative API, our first instinct is to wrap it inside a declarative API. By doing this, we make it easier for our application code to stay clean and declarative as it grows in complexity.

divider


Top comments (1)

Collapse
 
jkprasad profile image
jkprasad

Got to learn something new. Thanks!