Why I like Signals in Angular and the importance of Declarative code

Michele Stieven - Mar 2 '23 - - Dev Community

I've been reading some great posts from very smart folks, about React, about Solid, and about Angular.

I basically love them all (both the frameworks and the folks!). I mean, I'm not a Hook enthusiast... but who cares.

You've heard the big news: Signals are coming to Angular! I love this for many reasons, but I want to focus on one important thing here: declarative code.

This article was inspired by an example of a React component from Dan Abramov:

// React
function VideoList({ videos, emptyHeading }) {
  const count = videos.length;
  let heading = emptyHeading;
  if (count > 0) {
    const noun = count > 1 ? 'Videos' : 'Video';
    heading = count + ' ' + noun;
  }
  return <h1>{heading}</h1>
}
Enter fullscreen mode Exit fullscreen mode

Pretty simple! It accepts an array and a placeholder value, it does some calculations, and returns some text.

  • No videos here
  • 1 Video
  • 2 Videos

That's it, those are the combinations.

Now: I love Functional Programming, and React kinda follows a functional mindset.

f(state) = UI
Enter fullscreen mode Exit fullscreen mode

I won't be talking about hooks here: it's not the point.

The point is that Functional Programming is a subset of Declarative Programming. And I love declarative code. And that particular component's example is something that I don't quite like.

In this case, that component was a trivial example to demonstrate a concept. But in reality, I find that kind of code in many projects, so let's start from there to talk about Declarative code and why I'm excited for Signals in Angular.

This post assumes a basic knowledge of Signals (and a bit of React for the comparisons)!

Functional is Declarative

Let's get back to the component:

// React
function VideoList({ videos, emptyHeading }) {
  const count = videos.length;
  let heading = emptyHeading;
  if (count > 0) {
    const noun = count > 1 ? 'Videos' : 'Video';
    heading = count + ' ' + noun;
  }
  return <h1>{heading}</h1>
}
Enter fullscreen mode Exit fullscreen mode

Why don't I like it? Because this component's logic is imperative. This is not a problem with a component this small, but this is not how most components in complex codebases look like.

In order to understand what heading could be, I have to dig into all the rest of the component.

In pure functional programming, there are no variables, only constants. In JavaScript we can mutate variables, and we don't have to strictly follow FP's guidelines in order to write good code, but it's still a good recommendation in many cases.

This would be the declarative approach:

// React (declarative)
function VideoList({ videos, emptyHeading }) {
  const count = videos.length;

  const heading = count > 0
    ?  count + ' ' + (count > 1 ? 'Videos' : 'Video')
    :  emptyHeading;

  return <h1>{heading}</h1>
}
Enter fullscreen mode Exit fullscreen mode

Each declaration contains all the logic for that particular constant. When I read const heading = this_thing, in my mind I read "This is what heading is all about". I don't care about the rest of the component.

The disadvantage here is that the ternary operator could be more difficult to read and a bit awkward to write. We also cannot really use intermediate variables (such as noun in the previous example) without strange syntaxes like IIFE's.

But this to me is a small disadvantage compared to what we get. Now, at first glance, I know what heading is all about. The declaration contains the full recipe on how to calculate that value. I no longer have to read the rest of the component to understand it.

This is especially important for me as a consultant: I need to be able to look at some code and immediately understand the logic behind it. Why? Because I cannot afford to understand the whole project, nor can the client! Time is money.

And this is true for the teams, too! If I can understand something immediately, it means that the people behind that code will be able to maintain it well.

Classes

Compared to pure functions, a class is a different mental model. It's not a potential flow of instructions, but a group of Properties and Methods.

This means that the previous React component (the imperative one) cannot be written in the same way if we use a class. We'd have to place the logic inside a method. It'd be both ugly and strange.

// Angular (kinda, you get the point)
class VideoListComponent {

  @Input() videos;
  @Input() emptyHeading;

  count = 0;
  heading = '';

  ngOnChanges(changes) {
    this.count = changes.videos?.currentValue.length;
    this.heading = this.count > 0
      ?  this.count + ' ' + (this.count > 1 ? 'Videos' : 'Video')
      :  this.emptyHeading;
  }
}
Enter fullscreen mode Exit fullscreen mode

Honestly? This sucks. Compare it to how clean the previous React examples were. I don't want to touch Lifecycle methods. I don't care. I want to declare derived values: I want to write a recipe.

With classes, we can take advantage of getters in order to declare derived states.

class VideoListComponent {

  @Input() videos = [];
  @Input() emptyHeading = '';

  get count() {
    return this.videos.length;
  }

  get heading() {
    if (this.count > 0) {
      const noun = this.count > 1 ? 'Videos' : 'Video';
      return this.count + ' ' + noun;
    }
    return this.emptyHeading;
  }
}
Enter fullscreen mode Exit fullscreen mode

Much simpler to read and to reason about. That's what I suggest in such simple use-cases.

Notice that, since the getter is a function, it's easy to use intermediate variables (noun) and some sparkles of imperative code (which is self-contained and not scattered around the whole component).

Of course, it's a bit verbose to write. And we have to ask ourselves: will the framework pick up the changes? We are relying on Change Detection for those getters to work, and they could be fired potentially many times because there's no real way to know that a mutable property has changed.

That's about to end once we get Signals.

Signals

class VideoListComponent {

  // I think this is what we'll end up with, reactive
  // inputs as signals with default values, or something like that!
  videos = input([]);
  emptyHeading = input('');

  count = computed(() => this.videos().length);

  heading = computed(() => {
    if (this.count() > 0) {
      const noun = this.count() > 1 ? 'Videos' : 'Video';
      return this.count() + ' ' + noun;
    }
    return this.emptyHeading();
  });
}
Enter fullscreen mode Exit fullscreen mode

This is still more code than the function-based alternatives. Not much really. But:

  • It's very explicit
  • It's declarative

Also:

  • It's performant by default (as any other Signal implementation really), which I think is the future of any framework. I'm not a fan of the mindset of "optimize it later, if you need it", because in large applications I may not realize how expensive a specific computation will be in 3 months from now with a lot more code. And it'll be difficult to come back to the incriminated code.
  • We won't need zone.js anymore (phew)

These are all great benefits to me.

Signals have many flavors, one for each framework using them. Compare it to Solid's approach:

// Solid
function VideoList(props) {
  const count = () => props.videos.length;
  const heading = () => {
    if (count() > 0) {
      const noun = count() > 1 ? "Videos" : "Video";
      return count() + " " + noun;
    }
    return props.emptyHeading;
  }
  return <h1>{heading()}</h1>
}
Enter fullscreen mode Exit fullscreen mode

My opinion is that this approach is also great. But it has some small caveats.

  • Why are there arrow functions everywhere? We have to know that in Solid, in order to make a variable reactive, it must be a function. But this is a bit difficult to districate at first sight.
  • When I see a function, I don't know if it's a reactive derived state or an event handler. They look the same, so I have to read the code to know what it does (or, hope that the variable has a great name!).

It's important to note that this is just an opinion and you may like Solid's code more than anything else! It's a great tool.

So why do I like Angular with Signals? And why do I like working with classes this way?

Because with Angular's signals, we are forced to communicate that a value is a derived state. And we'll be forced to say that an input should be treated as a signal (if we want it to be!). And writing imperative code, such as the very first example of this article, is pretty much discouraged by the nature of classes in a reactive context. If your code looks bad, you're probably going to hate it, maybe tomorrow, maybe in 3 months.

With React, we have the possibility (which, however, I don't want) to write imperative code. We have a nice and short syntax. But we don't have fine-grained reactivity and Hooks have rules (I think it's fair to say that React's mental model is great until you add hooks, and they're used a lot).

With Solid, we usually deal with a lot of anonymous functions, and handling props can be quite strange (they're reactive, but we get them as normal values, destructuring is strange).

With Angular, we have a clear syntax and we're pushed towards declarative code. The tax to pay is a bit more code than the alternatives.

My opinion has been the same for many years: less code is not always objectively better. Classes can be easily understood by any high-school student who has learned OOP. They're easy to reason about if we do it right. So do it right :)

Many people criticize classes. They have some good reasons to do so. So how come that I appreciate them even if I love functional programming?

If we use classes with a bit of FP mindset (immutability, properties are constants), a lot of their cons disappear.

I'm happy that I won't ever need an ngOnChanges again. Or some input-setters to set some BehaviorSubjects values!

PS. Please, don't take this as a critique on React or Solid.

Photo by Tsvetoslav Hristov on Unsplash

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .