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>
}
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
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>
}
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>
}
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;
}
}
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;
}
}
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();
});
}
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>
}
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 BehaviorSubject
s values!
PS. Please, don't take this as a critique on React or Solid.
Photo by Tsvetoslav Hristov on Unsplash
Top comments (14)
The proposal makes a good first impression, especially the interoperability with RxJS looks promising.
For the rather large Angular codebase I am working on, I would't start integrating a development preview API, and even if they make signals stable, I won't go over hundrets of components until I can reap the the real benefit, which is not there yet: getting rid of zone.js. But zoneless application are in my opinion the most interesting change that is on the Angular roadmap since Ivy. Might also be worth to reevaluate ther angular-elements API when all this comes together. The last time I looked at it, the custom-elements had to bundle so much framework code that they became to bloated for the purpose of reuse outside of an angular app.
Sure you may want to wait! That's perfectly fine. However, it's not that there are no benefits other than the removal of zonejs. Like:
state$.next(state$.getValue() + 1)
becomesstate.update(s => s + 1)
source$ | async ?? defaultValue
everywhere (or using| null
on every input)I found Angular Elements useful for migrations, other than that, if I'm writing a Custom Element from zero, I'd suggest something like Lit which is a fantastic library!
Fair points, although in my particular setting, less pressing issues.
Starting from zero was what we did seven years ago (angular 2 beta.7 - a point in time where standalone components were not new because the NgModule was not there yet). The company has grown since then quite a bit and my thinking therefore is in the direction on how to pontially be able to share the internal ui component library (or with nearly 200 components in it, a specialized subset thereof) over with e.g. teams that got acquired without them having to re-write their UI completely.
I'm not meaning to start any sort of flame war here — at all — but isn't this remarkably similar to Vue 3's setup function syntax? Like, if you just put those
input()
's into adefineProps
(if using script setup) and pull it out of the class (including all of the necessarythis
removal and such) then you have a Vue3 component.I only bring this up to ask: are the frameworks converging on a common, good idea and syntax?
Yep it is indeed very similar! This type of reactivity is not new for Vue, and the fact that it uses objects instead of functions (React, Solid...) makes it easy to draw parallels with classes!
About converging, I really don't know! Each flavor has its merits and strengths, the React team for example appears to be very much against this type of reactivity, they like React's mindset better, so I guess they'll go in that direction. Who knows in the future! :)
That's what I thought when reading the article: go vue3 in component api mode and you will have what you're hoping for ;)
We could create a pipe for
heading
calculation. Functional, declarative, change detection friendly.That's a lot of extra code though, imagine writing that for each and every derived state! 😅 I suggest using Pipes for reusable utilities rather than specific use cases!
I agree it's a lot of boilerplate code but I think pipes can make components a lot simpler. I highly recommend them. Especially with the standalone pipes it's much less boilerplate code.
That's because it's 9 lines of code! I cannot use 500 lines of code to demonstrate a concept, no one would read the article. But that would be the reality, and I stand by my opinion that that'd be waaaaay harder to read: I do it every day :)
The performance tradeoff is negligible at best. FP trades a bit of performance (99% of the times it's unnoticeable) for declarativeness.
I'm sitting in the same boat and the tide is changing for Angular.
If you don't use classes with mutable properties, where are you supposed to put the state?
As my uni prof. used to say "a completely functional programmed software is a completely useless software"
And if you answered the question with "Redux" then well... think that a redux state can be implemented with a class :D
With Signals, the mutable state is inside the signals. State is mutable but signals are not.
Your prof was right, that's why there's no such thing as a useful completely functional program. It'd do nothing. Executing code by itself is a side-effect. The point of FP is not to avoid side-effects, but to isolate them. Or, wrap the effect so that it runs only when necessary (lazy evaluation), this gives you the ability to compose them without running them.
I agree 100%! :)