If you have been using Angular for a while, you may have heard terms like "reactive primitive", or "RxJS interoperability", or "declarative programming" a lot recently. I trust that you, at least on some level, understand what all of those mean; however, when it comes to real life, it is sometimes quite challenging to understand if a given piece of code is "reactive", "declarative", or whatever. It is even harder to change existing code to be reactive or declarative.
With this series of articles, I will try to explain all of these concepts not only theoretically, but with actual examples, real-life scenarios, and some tips that will help you recognize patterns associated with reactivity, start writing code with a declarative approach in mind from the very beginning, and understand the limitation of these approaches.
Also, we will try to make reactive programming less scary for developers who find themselves intimidated by bombastic-sounding concepts like "derived state", "side effect", and so on. For this purpose, this article will have callout notes to fix when we have learned a new term to keep it in mind and apply later.
Note: In these articles, I will touch different topics and terms from reactive programming, functional programming, and related fields; these articles are not comprehensive resources on those subjects and are limited to Angular applications; however, whenever appropriate, we will use some concepts from the aforementioned topics to better illustrate our approach. Do not worry - we will explain them all!
So, what is reactive programming?
I am tempted to say "reactive programming is when we react to something", and, honestly, that would not have been entirely wrong. However, there is more to it than that.
I could say "reactive programming is when we react to events", and that would not have been entirely wrong either. But imagine someone who understands reactive programming looking at this code:
document.body.addEventListener('click', e => {
console.log('clicked');
});
Will they sincerely call this code "reactive"? After all, it is reacting to an event, and we just gave two definitions with both of which this code is perfectly compatible. So what's the catch?
Maybe, it will be easier to talk about what reactive programming is not, and from that we can at least have a tool that will help us deduce if our code is reactive. In further sections, you will notice that often explaining some terms related to reactive programming involves figuring out what the term does not mean, so this pattern will repeat itself a lot in these articles.
With this in mind, let's talk about data.
What is "reactive state"?
Often, when we refer to data in frontend applications (but not exclusively), we call it "state". Why is that? Well, because the data (which is constantly changing during the lifetime of an application), has different values at different points in time, and we can only interact with a "snapshot" of that data at a given point in time. That snapshot is what we refer to as "state".
In frontend applications, we usually have some initial state, a UI that is rendered based on that state, and events that trigger changes to that state, which in turn results in a new UI. But before we dive deep into that, let's first figure out the concept of "state" in general programming, not just frontend or any framework in particular.
Let's discuss this very simple piece of code:
let firstName = 'John';
let lastName = 'Doe';
let fullName = firstName + ' ' + lastName;
console.log(fullName);
So, is this code reactive? Not really. While at first (on line 3), fullName
does contain the actual fullName
value, we can easily throw it off by adding a simple line of code:
let firstName = 'John';
let lastName = 'Doe';
let fullName = firstName + ' ' + lastName;
console.log(fullName); // logs 'John Doe'
firstName = 'Jane';
console.log(fullName); // still logs 'John Doe'
Well, obviously the fullName
variable did not react to the change in firstName
. Of course, this is expected, these are just variables - small pieces of data that are constantly changing.
Now, while this example is probably the simplest ever, what it did is actually define our problem (or at least a part of it) that we aim to solve with reactive programming, and that is keeping data in sync.
Now, let's look at this example:
class Person {
constructor(public firstName, public lastName) {}
get fullName() {
return this.firstName + ' ' + this.lastName;
}
}
const person = new Person('John', 'Doe');
console.log(person.fullName); // logs 'John Doe'
person.firstName = 'Jane';
console.log(person.fullName); // now it logs 'Jane Doe'
Here, we did a clever trick; instead of storing the fullName
in a variable, we realized that we don't actually need to "store" it, we just need to calculate it whenever we need it. This is only possible because fullName
itself is not actually any data; it is just a representation of the actual data (firstName
+ lastName
).
In this case, the fullName
"property" is fully dependant (note the word "fully" here, in the future, we will encounter cases where some state is only partially dependent on other state) on the firstName
and lastName
properties, so we do not need to store it, we can just compute it. Such computed states are often called "derived" states. Remember this term if it is unfamiliar to you.
New term: Derived state - data that is dependant on other data, and that is updated every time one of its dependencies changes.
So, is this reactive code? Not quite yet; the issue is, we learned we can derive new state based on other state, but what if our "reaction" to some state is not to simply compute, but perform a side effect?
Wait wait wait, side effect. What is this? Yet another "scary" term. Let's figure it out.
What is a side effect?
To understand this, we need to talk about functional programming a tiny bit, mainly about the concept of pure functions.
Let's talk about a particular function. This one:
let count = 0;
function incrementInFiveMinutes() {
setTimeout(() => count++, 5_000 * 60);
}
Now, this incrementInFiveMinutes
function is not a pure function. Let's understand why. To do this, let's add some code:
incrementInFiveMinutes();
document.querySelector('button').addEventListener('click', () => {
console.log(count);
});
Now, let us ask ourselves a question. What number will be logged in the console? If you answered "it depends!", then surely you are getting to the point. Of course, if we run this program at 17:51, and click on the button at 17:53, it will log 0
, but if we click again at, say, 18:07, it will log 1
.
However, our function ran once at 17:51 and since then, long stopped being any part of the program, however it still impacted the way our application functions. Now, let us consider the following code:
let first = 7;
let second = 3;
function sum(a, b) {
return a + b;
}
console.log(sum(first, second));
Now, let us ask the same question - what will be logged in the console? Well, obviously, it will be 10
, because first
and second
are both 7
and 3
respectively, and sum
just returns the sum of those two numbers. Now, it was very easy for us to see this, because sum
is, in fact, a pure function. It only takes any data in the form of arguments, does not modify them in any way, and does not touch any other data in the outside world.
It is easy to see why pure functions are so great; as we just saw, they are very predictable, we do not need to run them to see what they do, and when explaining them, we never have to say "well, it depends".
New term: Pure function - a function that does not modify any data in the outside world, and that only takes data as arguments.
Now, let us go back to the incrementInFiveMinutes
function and see what it is that made it "impure". Well, we scheduled a timeout, which is a mechanism that is outside of this function - obviously setTimeout
is defined elsewhere and is a global function, and, furthermore, we also changed the value of count
in the callback, which is also a global variable. So, we indirectly impacted the application state or some external system. This is what a side effect is!
New term: Side effect - a change in the state of the application or some external system that is not directly related to the function's arguments or return value.
With this in mind, we can simplify the "pure function" definition.
Updated term: Pure function - a function that does not have any side effects
It is important to understand that functional purity is a very strict concept. It is very easy to break a function's purity, simply by referring to something out of its scope. Furthermore, while pure functions are predictable, beautiful, safe and so on, it is impossible to build a useful frontend application with only pure functions. Take a look at this simple Angular code:
@Injectable({providedIn: 'root'})
export class ProductService {
readonly #http = inject(HttpClient);
addProduct(product: Product) {
return this.#http.post('my.api.com/products', product);
}
}
Now, the addProduct
method definitely has a big side-effect: it modifies a database that is probably located on another continent. However, it is also a very important function, as it allows us to build applications that let users add, delete, update and otherwise manage products.
So, as we can see, "pure functions" is not about having only pure functions, but rather, about separating "pure logic" from "data modification" or other processes that we have to do just because we work with computers.
Now, having learned about pure functions and side-effects, we can finally add the missing puzzle of "reactive state". If we bring ourselves mentally back to the end of the previous subsection, we will remember that we defined "derived state" and mentioned that state being "derivable" is not enough to call it reactive, and we need to be able to also perform side-effects whenever that state changes.
We brought forth the example of writing a reactive value in a way that its value will be stored in localStorage
, which, as a keen reader might notice, is also a side effect. So, how about we try to create a reactive value wrapper such that it will be possible to derive new reactive values from it, and also to perform side-effects whenever that value changes, all by ourselves?
Turns out, we can make a primitive version of this pretty easily.
export class ReactiveValue<T> {
#value: T;
#sideEffects: ((value: T) => void)[] = [];
constructor(value: T) {
this.#value = value;
}
getValue() {
return this.#value;
}
setValue(value: T) {
if (this.#value !== value) {
this.#value = value;
this.#sideEffects.forEach(sideEffect => sideEffect(value));
}
}
onChange(sideEffect: (value: T) => void) {
this.#sideEffects.push(sideEffect);
}
}
Now, with this code, we can create a reactive variable and do whatever we want in side effects:
const count = new ReactiveValue(0);
count.onChange(value => localStorage.setItem('count', value));
count.setValue(7)
Warning: this code is simply a demonstration, and is heavily simplified; do not use it in production-ready applications; if you are using Angular, utilize reactive primitives provided bt the framework instead (signals, linked signals, and so on).
Now, what can we see here? Well, we can create a reactive state, update it, and be sure that its updates will trigger relevant side effects. This is a big step towards understanding reactive programming, but it is not final - we have yet one final concept to learn before we can say that we grasp the fundamentals of reactive programming, and that is declarative code.
Declarative Code
To understand declarative code, we first need to take a look at spaghetti.
No, not that spaghetti! This spaghetti:
@Component({...})
export class ProductComponent implements OnInit, OnChanges {
@Input() productId: number;
readonly #productService = inject(ProductService);
readonly #userService = inject(UserService);
product: Product;
currentUser: User;
ngOnInit() {
this.#userService.getCurrentUser().subscribe(
user => this.currentUser = user,
);
}
ngOnChanges() {
this.#productService.getProduct(this.productId).subscribe(
product => this.product = product,
);
}
}
While this is a small example and we might even wonder "why is this code so bad to call it "spaghetti"?", we can think about it in terms of reactivity (so far as we understood it), and figure it out for ourselves.
- The component has an input of
productId
product
depends onproductId
, as we cannot load a product without its id- We can then say that
product
is reacting toproductId
- We also have a
currentUser
property which is being loaded separately.
Now, this is the sort of code that one might call "imperative", which is the opposite of "declarative" in a broad sense and is a (relatively) "bad" thing for the lack of a better words. Let's deconstruct what "imperative code" means using a simpler example:
const users = [
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 },
{ name: 'Bob', age: 35 },
{ name: 'Alice', age: 16 },
];
let result = [];
for (const user of users) {
if (user.age > 18) {
result.push(user);
}
}
Now, this is also some code that I would call "imperative". So why is that? Well, to understand it, let's pretend we did not write this code, and are instead a developer who is reading this piece of code. Here are the mental steps we would probably take:
- Okay, there is an an array of user objects
- Hmmm, there is a new, empty array called
result
- Mental detour: probably some logic is going to be performed on the
users
array, and the result is going to be accumulated in theresult
array - Okay, there's a
for
loop, let's see what it does - Okay, it checks if the user is older than 18... I see where this is going...
- Yeah, sure, it pushes it in the
result
array! Okay, got it, it filters adult users
Now, with this code, we went through 6 mental steps with one detour to understand what it does. This is because this code describes how to filter adult users.
Now, let use see the same code, but better:
const users = [
{ name: 'John', age: 30 },
{ name: 'Jane', age: 25 },
{ name: 'Bob', age: 35 },
{ name: 'Alice', age: 16 },
];
let result = users.filter(user => user.age > 18);
Now, not only is this code shorter, it is also simpler to both explain and digest. Let's do the same mental exercise:
- Okay, there is an an array of user objects
- Ah, we are filtering this array... let's see on what condition
- The condition is
age > 18
... ah, okay, we filtered the adult users!
As we can see, grasping this piece of code is roughly 2 times easier in terms of mental steps we take than the previous one. Also notice that we purposefully named the new array result
and not adults
or adultUsers
, as we would do in real life, to further illustrate that the second approach is superior to the first one even when the namings aren't quite great.
Okay, so what is it then which makes the second approach better? Actually it is fairly simple to explain.
The first example is describing, step-by-step, how to filter the adults from a list of users; we need to first read the implementation to deduce what it does.
The second example, on the other hand, is explicitly telling us what it is doing; the process of how it is exactly filtering the adults from the users
array is hidden (and, frankly, unimportant), which is why we almost immediately understand what it tries to achieve.
And this is, for the most part, the definition of declarative and imperative code styles.
New term: imperative code: a piece of code which describes, in distinct programming steps, how to achieve a result which the program wants to achieve
New term: declarative code: a piece of code which describes, usually in one or few steps, what the program wants to achieve
Now, let us notice several things about these concepts:
- Declarative programming almost always involves some level of abstraction: for instance, the
filter
method abstracts away the process of setting up a new array, looping thorough the existing one, performing the checking and then pushing the results in this new array. We only provide the namefilter
and the condition to check, and the method does the rest. - Imperative code can be transformed in a way that it becomes (kind of) declarative. The best example of this is creating functions (especially pure functions) that contain the imperative code, and then using those functions. For instance, in the first example we might have kept the code the same and just put it into a function named
filterAdults
, and it would have been more declarative than what we had initially - Both terms are not super rigid and involve a "spectrum"; this is why we used the word "more declarative" earlier
- Despite those terms not having precise scientific accuracy, they are very useful in helping us evaluate the quality of our code
Now, we learned two definitions that will help us understand why the previous Angular component is poorly written (you guessed it - it is imperative code).
Let's review the same example, now with comments explaining step-by-step where the problems are:
@Component({...})
export class ProductComponent implements OnInit, OnChanges {
@Input() productId: number; // no problems here, components sometimes have to have input properties
readonly #productService = inject(ProductService); // just DI
readonly #userService = inject(UserService); // just DI
product: Product; // okay, we have a definition of a property called `product`, but it's just s definition; there is no way to understand what it contains, how it value is updated, and so on
currentUser: User; // same here, `currentUser` seems to be produced from thin air
ngOnInit() { // using `ngOnInit` itself is not reactive in terms that it doesn't really explain what is going to be done
this.#userService.getCurrentUser().subscribe( // imperative code - we directly subscribe to the observable
user => this.currentUser = user, // imperative code - we explain *how* the `currentUser` property receives its value
);
}
ngOnChanges() { // same
this.#productService.getProduct(this.productId).subscribe( // same
product => this.product = product, // same
);
}
}
With this, so far, we learned about reactive values, derived state, pure functions, side effects, and declarative code. This is a great base to finally define what reactive programming is, and to do that, we will again use examples to reconcile these concepts into one coherent understanding of the topic.
Defining reactive programming
So far, all we did was talk about state - data that changes over time. However, and this is important, not everything is a state. Let's see the following simple example to understand what we mean by that:
document.body.addEventListener('click', () => {
console.log('clicked');
});
In this case, we do not have any state, but we do, however, have a side effect. So, what triggers the side effect, if not a state change? Well, obviously, an event. Now, events are a bit abstract in the sense that it is hard to very clearly define what is and what isn't an event. Clearly, a user clicking a button is an event - an arising asynchronous thing that may trigger action down the line. But, some may argue that a change to reactive state, which we described above, is also an event, which, on the surface, makes sense.
However, if we are going to do proper reactive programming, we need to be able to distinguish between events and reactive state changes. This is because of two important distinctions, which we will discuss right now.
Events and state change - what's the difference?
First of all, an important distinction is that state always has a "current" value. In the example above, we created a reactive variable, and it had a value, and when we changed it, not only the side effects were triggered, but the value of the reactive variable changed as well. And we could, always, without having to rely on side effects, read the current value of the reactive variable.
It is way different when it comes to events. The very previous example we used, with the click event, does not have a concept of "current" value. If we think about it, well, what is the "current" click? We could introduce such a concept by storing the last click event, but that is only a small subset of what we could do there, because, well, how is it better and more useful than the "first" click event? So, in that sense, events, compared to state changes, are ephemeral (for the lack of a better word) - they exist momentarily and pass away as soon as we are done reacting to them. If we continue this line of thinking, we can kind of say that the addEventListener
handler is a "pure side effect" (as much as it sounds self-contradictory!), because it handles an event, but is not associated with any state.
Next, there is a more technical distinction between events and state changes. Events are by design asynchronous - we cannot predict when they can happen, even if at all (user might not click anywhere forever, for instance). On the other hand, state changes are synchronous - any time we update the state, the associated side effects are triggered, and derived states can be updated immediately too (this is not always the case in real life, as we will see in the next article).
It is important to understand that both of these distinctions, while more or less strict, are not always like that. Sometimes, some reactive concepts exist on the edge between the two. Take a look at this example:
let todos = [];
fetch('https://jsonplaceholder.typicode.com/todos')
.then(res => res.json())
.then(result => {
todos = result;
});
Now, the second callback of the then
method is clearly a side effect, data coming from an external system (backend API) modifying a local variable, but it also is associated with a reactive value - the todos variable, which we could simply modify further down the line. This scenario is important for our further explorations in the next article.
Finally, we are ready for a sufficient definitions of reactive programming!
Reactive programming is the practice of writing software wherein application data (the state) is reactive, meaning we can perform side effects on their state changed and derive new state from previous reactive values, and where we react to both asynchronous events and synchronous state changes in a declarative manner.
This is kind of a mouthful, which is why I prefer a shorter (joke) definition: reactive programming is when we react to things declaratively.
Finally, before wrapping up this first article, let's talk about reactive tools that Angular provides to solve these sorts of problems - after all, the article is titled "Reactive Programming in Angular", and we have not talked about Angular too much yet.
Reactive Tools in Angular
Now, here, we leave the realm of abstract concepts in favor of quite strict definitions. Angular has exactly 2 toolsets for reactive programming: RxJS and Signals. Let's briefly discuss both of them.
RxJS
RxJS should be strictly used for handling events and not reactive state changes. Note that we say "should", as it is very possible to use RxJS for reactive state changes, but it is not the best practice - RxJS is more tailored for handling streams of events, rather than state. It is quite a rich toolkit (async programming usually is harder, we deal with timing, cancellation, async errors, hence more complexity).
However, the big thing about RxJS is that it allows handling events in a declarative fashion. Remember the addEventListener
example? Well, it was obviously quire imperative, (we told how to handle an event), and not very extendable (we couldn't really modify the event listener function after defining it).
Instead, with RxJS, we can declare a stream of event clicks, and use it in multiple ways however we see fit:
const clicks$ = fromEvent(document, 'click');
clicks$.subscribe((event) => {
console.log('Clicked');
});
Here, instead of imperatively telling the program how to handle the event, we declared a stream of events, and then described what we are doing to it. We can also derive new streams from this existing one, as our definition of reactive programming tells us:
const clicks$ = fromEvent(document, 'click');
const ctrlClicks$ = clicks$.pipe(filter(event => event.ctrlKey));
ctrlClicks$.subscribe((event) => {
console.log('Ctrl-clicked');
});
Here, we have a stream of clicks, and we derived a new stream of clicks that only include clicks with the ctrlKey
pressed. The original stream is not modified in any way, and if we wish, we can subscribe to it separately and do something with all clicks, or, instead, we can work only with those clicks that have the Ctrl
key pressed.
So, RxJS is a remarkable tool for working with asynchronous events in a declarative manner - fantastic reactive library! We will talk a lot about it in subsequent articles.
Signals
Signals are a relatively small toolkit (at least when compared to RxJS), which allows us to define a reactive state, update it, derive new reactive states from it, and perform side effects upon its state changes. As we can see, Signals, on their part, are reserved strictly for reactive state, and not asynchronous events. If you worked with the latest versions of Angular, you will be at least somewhat familiar with them, but here is a small example:
@Component({
template: `
<button (click)="decrement()">-</button>
<span>{{ count() }}</span>
<span>{{ doubleCount() }}</span>
<button (click)="increment()">+</button>
<button (click)="reset()">Reset</button>
`
})
export class MyComponent {
count = signal(0);
doubleCount = computed(() => this.count() * 2);
constructor() {
// save the latest count to local storage
effect(() => {
localStorage.setItem('count', this.count());
});
}
increment() {
this.count.update(n => n + 1);
}
decrement() {
this.count.update(n => n - 1);
}
reset() {
this.count.set(0);
}
}
As we can see, all the necessary conditions for reactive programming are there, the value is created declaratively, we have derived state with computed
, and we have side effects with effect
.
Interoperability between the two
As we mentioned, handling both reactive state changes and events is crucial for having reactive code. Furthermore, as we also saw, some of the reactive values exist on the edge between events and state, like HTTP calls. So, keeping this in mind, it is important for great developer experience to be able to seamlessly switch between state and events, or, in case with Angular, from signals to RxJS and vice versa. Of course, this is possible with toSignal
and toObservable
functions and some other utilities, which we will explore in full detail in the next article.
Conclusion
In this article, we learned the theoretical basis (while looking at examples) of reactive programming, understood what state and derived state are, what are pure functions and side effects and why it is important to recognize them, learned about events and state changes and how they are related, and finally, introduced RxJS and Signals, the reactive toolkits available for Angular.
In the next article, we will start building software by actually using those tools, enhancing our newly acquired knowledge with some work in the fields.
Small promotion
In this article we discussed signals and RxJS interoperability. The recent upgrades in reactive programming in Angular have caused many developers to be confused about what solutions to chose, how to implement them, and how to migrate their existing codebases to the most recent features. Thankfully, I have a response to this concern: very soon, my very first book is going into print!
It is called "Modern Angular" and it is a comprehensive guide to all the new amazing features we got in recent versions (v14-v18), including standalone, improved inputs, signals (of course!), better RxJS interoperability, SSR, and much more. If this interested you, you can find it here. The book is now in the copy-editing phase with a release scheduled shortly, so it is currently in Early Access, with all the 10 chapters already available online. If you want to keep yourself updated on the print release, you can follow me on Twitter or LinkedIn, where I will be posting whenever there are news or promotions available.
P.S. Hey! Check out chapter 5 of my book to learn about RxJS interoperability and chapters 6-7 for a deep dive into signals ;)