RxJS

Reactive programming in Angular 101

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 q

Reactive programming in Angular 101 — RxJS article by Armen Vardanyan on Angular In Depth
Reactive programming in Angular 101 — RxJS article by Armen Vardanyan on Angular In Depth
On this page · 11 sections

If you have spent any time with Angular, you have likely come across phrases such as "reactive primitive," "RxJS interoperability," or "declarative programming" in recent discussions. I am confident that you have a certain level of familiarity with these terms; yet, in practice, it can be tricky to tell whether a particular snippet of code truly embodies the ideas of "reactivity" or "declaration." The challenge becomes even greater when you need to refactor existing code to align with those principles.

In this series, my goal is to demystify these concepts not just in abstract terms, but by walking through concrete examples, realistic use cases, and practical advice. You will learn to spot traits associated with reactive systems, adopt a declarative mindset when starting new projects, and grasp the boundaries or trade-offs inherent in such methods.

Beyond that, this series aims to soften the intimidation factor around terms that often sound overly complex—like "derived state" or "side effects"—for developers who might feel put off by their grandiosity. To support that goal, this installment includes callout-notes where we pause to formally define each new term as it appears, so you can reference them later.

Note: Throughout these posts, we will dip into various topics from reactive programming, functional programming, and adjacent areas; the coverage is not exhaustive and stays focused on Angular applications. That said, when relevant, we will borrow concepts from those fields to clarify our reasoning. Rest assured—every term will be flagged and explained!

Defining Reactive State

There is a strong temptation to describe reactive programming as simply "responding to something," and that description is not entirely off base. However, a more precise understanding is needed.

Another common shorthand is "reactive programming is reacting to events," which is also partially accurate. Yet, consider a developer who is well-versed in reactive principles looking at this snippet:

document.body.addEventListener('click', e => {
  console.log('clicked');
});

Would they genuinely classify that code as "reactive"? It is, after all, responding to an event, and it aligns perfectly with both of the earlier definitions. So, where is the distinction?

Perhaps it is more effective to examine what reactive programming is not. By establishing these boundaries, we gain a practical tool for evaluating our own code. As you will see throughout this series, understanding what a term does not encompass is often a key step in grasping its true meaning.

With that approach in mind, let's begin with data.

The Nature of State

In frontend development, the data we manage is frequently referred to as "state." Why? Because this data is mutable throughout the application's lifecycle. At any given moment, we interact with a particular "snapshot" of the data, and that snapshot is what we define as the "state" at that point in time.

Typically, a frontend application has an initial state, a user interface rendered from that state, and events that trigger state transitions, which then lead to a new UI rendering. Let's first explore the concept of "state" generically, without tying it to any specific framework.

Let's look at a very basic example:

let firstName = 'John';
let lastName = 'Doe';
let fullName = firstName + ' ' + lastName;

console.log(fullName);

Would you consider this code reactive? Not really. Although fullName initially contains the correct string, it is easily broken by a single addition:

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'

As expected, the fullName variable did not react when firstName changed. This is normal behavior for regular variables; they are just independent pieces of data.

This simple example highlights a core challenge that reactive programming aims to solve: keeping data synchronized.

Now, let's examine a different approach:

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'

This example represents a clever shift in thinking. Instead of storing fullName as a separate variable, we treat it as a value that is calculated on demand. This is feasible because fullName is not new data; it is a representation or a formula based on other data (firstName + lastName).

In this case, fullName is completely (and note the emphasis on "completely") reliant on firstName and lastName. Since there is no need to store this calculated property, we can compute it. State that is computed from other state is known as "derived" state. If this term is new to you, keep it in mind.

Key concept: Derived state - Data that is dependent on other data and updates automatically whenever its dependencies change.

Is this code reactive? We are getting closer, but it's not quite there yet. We have shown how to derive new state from existing state, but what if our reaction to a state change is not just a simple computation, but a side effect that alters something in the application environment?

Hold on, a side effect? That's another potentially intimidating term. Let's break it down.

Understanding Side Effects

To grasp this concept, we need to briefly touch upon functional programming, specifically the idea of pure functions.

Consider this specific function:

let count = 0;

function incrementInFiveMinutes() {
    setTimeout(() => count++, 5_000 * 60);
}

This incrementInFiveMinutes function is not a pure function. Let's examine why by adding some context:

incrementInFiveMinutes();

document.querySelector('button').addEventListener('click', () => {
    console.log(count);
});

Now, how can we predict what will be logged to the console? If you are thinking "it depends!", then you are on the right track. The result depends on the current time. If the program runs at 17:51 and the button is clicked at 17:53, it will log 0. If the button is clicked again at 18:07, it will log 1.

Notice that our function completed its execution long ago, but it continues to affect application behavior. Now, consider another piece of code:

let first = 7;
let second = 3;

function sum(a, b) {
    return a + b;
}

console.log(sum(first, second));

What will this log? In this case, it is straightforward: 10. We can easily predict this because first is 7, second is 3, and the sum function simply adds its two arguments. This predictability is the hallmark of a pure function. It receives data via arguments, doesn't modify them, and operates independently of the outside world.

The benefits of pure functions are clear from this example. They are highly predictable; we don't need to execute them to know their output, and we never need to respond with "well, it depends" when asked what they do.

Key concept: Pure function - A function that only takes data as arguments and does not modify any data in the outside world.

Let's revisit the incrementInFiveMinutes function to pinpoint what made it "impure." We scheduled a timeout using setTimeout, a mechanism defined outside the function's scope. Furthermore, the callback modified the value of count, which is also a global variable. The function influenced the application state or an external system. This is precisely what constitutes a side effect.

Key concept: Side effect - A modification to the state of the application or an external system that is a consequence of a function's execution, but not part of its explicit return value.

With this definition, we can refine our definition of a pure function.

Refined concept: Pure function - A function that has no side effects.

It is crucial to understand that functional purity is a very stringent concept. A function's purity is easily compromised by referencing anything outside its local scope. While pure functions are predictable and safe, a useful frontend application cannot be built solely with them. Look at this basic Angular example:

@Injectable({providedIn: 'root'})
export class ProductService {
    readonly #http = inject(HttpClient);

    addProduct(product: Product) {
        return this.#http.post('my.api.com/products', product);
    }
}

The addProduct method has a substantial side effect: it alters a database, likely on a distant server. However, this function is also essential, as it enables users to add and manage products.

Thus, "functional programming" is not about writing exclusively pure functions. Instead, it is about separating the core logic (which is pure) from the necessary data modifications and I/O processes that computers require.

Now, with a clear understanding of pure functions and side effects, we can fill in the final piece of the "reactive state" puzzle. Revisit the discussion on "derived state" to see why it was not sufficient for reactivity. The missing ingredient is the ability to perform side effects whenever that derived state changes.

Earlier, we considered an example of writing a reactive value whose update would also store it in localStorage. This is a side effect. Why not build our own wrapper to manage this? The goal would be to create a reactive value that allows deriving new values from it and also executing side effects upon change.

Remarkably, we can build a primitive version of this quite 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);
    }
}

With this in place, we can create reactive variables and use side effects freely:

const count = new ReactiveValue(0);
count.onChange(value => localStorage.setItem('count', value));
count.setValue(7)

Note: This is a demonstration for learning purposes and is heavily simplified. It is not intended for production code. In Angular applications, prefer to use the framework's built-in reactive primitives like signals or linked signals.

What have we accomplished here? We can now create reactive state, update it, and ensure its changes trigger necessary side effects. This is a significant stride toward understanding reactive programming, but it is not the final step. One more crucial concept remains: declarative code.

Writing Declarative Code

To begin, we need to contemplate spaghetti.

spaghetti.png

No, not that kind of spaghetti! This kind:

@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,
        );
    }
}

This is a small example, and one might wonder why it is considered so "spaghetti-like." But if we think about it from a reactivity standpoint (with what we've learned so far), the issues become clear.

  1. The component receives a productId as input.
  2. The product data is dependent on this productId; we cannot load a product without its identifier.
  3. Therefore, we can say product is reacting to productId.
  4. Similarly, the currentUser property is loaded independently.

This type of code is often labeled as "imperative," which is the opposite of "declarative." While these are broad terms, "imperative" is generally considered less ideal. Let's break down what makes code "imperative" 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);
    }
}

This code also strikes me as "imperative." But why? To understand, let's pretend we are a fresh developer reading this without prior context. Our mental process would be:

  1. Okay, we have an array of user objects.
  2. Ah, here's a new empty array called result.
  3. Mental note: Logic will likely be applied to the users array, with the output stored in result.
  4. We have a for loop; let's see what it does.
  5. It's checking if a user is older than 18... I have a hunch...
  6. Yes, it's pushing them into result. Got it: it filters adult users.

Reading this code took about six distinct mental steps, including a detour, to understand its purpose. This is because the code details how to perform the filtering process, step by step.

Now, let's see a more straightforward version of the same logic:

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);

This version is shorter and much simpler to grasp. Let's repeat our mental exercise:

  1. Okay, we have an array of user objects.
  2. We're filtering this array; let's see the condition.
  3. The condition is age > 18; so, it filters adult users.

The cognitive effort for this piece of code is significantly lower—roughly half the steps. And we intentionally named the new array result rather than something illustrative like adults to emphasize that this second approach is easier to understand, even when its naming is suboptimal.

So, why is the second approach superior? The explanation is quite simple.

The first example specifies, instruction by instruction, how to filter adults. To determine what it does, we must read the entire implementation.

The second example, conversely, explicitly communicates what it is doing. The underlying how is abstracted away (which is irrelevant to the requirement), making its purpose immediately apparent.

This encapsulates the core distinction between declarative and imperative styles.

Key concept: imperative code: Code that specifies, through distinct programming steps, how a desired result should be achieved.

Key concept: declarative code: Code that specifies, in one or a few high-level steps, what the program intends to achieve.

It's helpful to make a few observations about these concepts:

  1. Declarative programming almost always relies on abstraction. The filter method, for instance, hides the manual steps of array iteration and conditional insertion. We provide the result and the predicate, and the method handles the mechanics.
  2. Imperative code can be refactored to become more declarative. The most common method is encapsulating the imperative instructions within a well-named function. For example, we could wrap the for loop logic into a function named filterAdults, which would make its usage more declarative than the raw loop.
  3. These terms represent a spectrum, not absolute categories. Which is why we say "more declarative."
  4. Despite their lack of scientific precision, they are very practical tools for assessing code quality.

With this new terminology, we can pinpoint why the earlier Angular component was poor design (because it's heavily imperative).

Let's review that same component, now with annotations highlighting the specific problems:

@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
        );
    }
}

We have now covered reactive values, derived state, pure functions, side effects, and declarative code. These foundations are sufficient to provide a solid, comprehensive definition of reactive programming.

What is Reactive Programming?

Up to now, our focus has been on state—data that changes over time. However, it is essential to understand that not all relevant data is state. The following example makes this clear:

document.body.addEventListener('click', () => {
    console.log('clicked');
});

In this case, there is no state, but there is definitely a side effect (the click handler). What initiates the side effect if not a state change? An event. Events are somewhat abstract to define strictly, but a user's button click is a quintessential example: an asynchronous occurrence that can trigger further actions. One could argue that changing a reactive state is itself an event, which seems logical at first glance.

However, proper reactive programming necessitates a distinction between events and state changes. There are two key reasons for this distinction, which we'll explore next.

Events or State Change?

First, the most significant difference is that state always has a "current" value. In our earlier reactive variable example, it held a value. When we updated it, both the side effects ran and the underlying value changed. We could always, independently of the side effects, read its current value to know what it contained.

Events are very different. An event, like a click, does not have a "current" value. If you think about it, what would be the "current" click? We could designate a variable to store the last click, but that feels arbitrary. Why is the most recent click more useful or meaningful than the "first" or "second" one? In this sense, an event is a discrete, ephemeral occurrence. It exists for a moment and becomes unimportant after it is handled. Following this thought, a simple addEventListener callback can be seen as a "pure side effect"—it handles an event without presenting a value for a reactive state.

Second, there is a more technical difference: events are asynchronous by design. We cannot accurately predict when they will happen, if ever (maybe the user will not click for an hour). State changes, conversely, are synchronous. When state updates, all dependent side effects and derived states immediately recalculate (though this is not always the case in practice, as we will discuss in the next article).

Now, while these distinctions are generally sound, they are not absolute. Some reactive concepts exist in a gray area. Consider this:

let todos = [];
fetch('https://jsonplaceholder.typicode.com/todos')
    .then(res => res.json())
    .then(result => {
        todos = result;
    });

In this example, the callback for the then method is a clear side effect; data from a backend response modifies a local todos variable. However, the question of whether this is a reactive state change or purely an event reaction is complex. This type of scenario is vital for our discussions in the upcoming article.

We are now in a position to offer a robust definition of reactive programming.

Reactive programming is a development approach centered on making application data (state) itself reactive—allowing for the declaration of side effects on change and the derivation of new state from existing reactive values—and where reactions to asynchronous events are handled alongside synchronous state updates in a clear, declarative style.

That definition is quite a mouthful, which is why I prefer a shorter (and slightly whimsical) version: reactive programming is reacting to things declaratively.

Before we conclude this part of the article, let's discuss the specific tools Angular provides to handle these kinds of scenarios—considering this series is titled "Reactive Programming in Angular," it's time we focused more on the framework itself.

Angular's Reactive Toolkits

At this point, we step away from theory and move into concrete tools. Angular offers exactly two reactive programming utilities: RxJS and Signals. Let's take a closer look at each.

RxJS

RxJS is intended for managing events, not reactive state updates. The word "intended" matters here—technically, you can use RxJS to model state reactively, but it's not considered best practice. RxJS is built for event streams, and event handling brings its own complexity: timing, cancellation, asynchronous errors, and so on. That's why the toolkit is so extensive.

The key strength of RxJS is that it lets you handle events declaratively. Think back to the addEventListener example—it was imperative in nature (we specified exactly how to react) and rigid (once defined, the listener couldn't be easily modified or extended).

With RxJS, you define a stream of clicks and then decide how to consume it, in multiple ways if needed:

const clicks$ = fromEvent(document, 'click');
clicks$.subscribe((event) => {
  console.log('Clicked');
});

Notice the shift: instead of imperatively instructing the program how to respond to each event, we declare a stream and then describe what transformations we apply to it. And, following the principles of reactive programming, we can create entirely new streams derived from the original one:

const clicks$ = fromEvent(document, 'click');
const ctrlClicks$ = clicks$.pipe(filter(event => event.ctrlKey));
ctrlClicks$.subscribe((event) => {
  console.log('Ctrl-clicked');
});

Here, we start with a stream of all clicks, and from it we derive a new stream that filters for clicks where ctrlKey is pressed. The original stream remains untouched—you can still subscribe to it separately if you want to handle all clicks, or you can focus only on the Ctrl+click subset.

In short, RxJS is an excellent library for dealing with asynchronous events in a declarative style. We'll dive much deeper into it in upcoming parts of this series.

Signals

Signals are a much smaller utility compared to RxJS. They let you define state, update it, create derived state, and trigger side effects when state changes. In other words, Signals are designed specifically for reactive state—not asynchronous events. If you've used recent versions of Angular, you're likely familiar with them. Here's a minimal 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);
    }
}

All the ingredients of reactive programming are present: state is declared upfront, derived state is produced with computed, and side effects are registered with effect.

Moving Between the Two

As discussed earlier, real-world reactive code needs both state management and event handling. Plus, some reactive values sit at the boundary—HTTP calls are a good example. For a smooth developer experience, you need to easily go back and forth between Signals and RxJS. Angular provides helpers like toSignal and toObservable (along with a few others) for exactly this purpose, and we'll cover them in depth in the next article.

Wrapping Up

In this part, we laid the theoretical groundwork with practical examples. We defined state and derived state, explained pure functions and side effects and why distinguishing between them matters, explored events and state changes and how they connect, and finally, introduced RxJS and Signals as Angular's two reactive tools.

In the next article, we'll get our hands dirty and start building with these tools, cementing the theory with real practice.

A Quick Word on My Book

Modern Angular.jpeg
The recent wave of reactive updates in Angular has left many developers unsure which approach to adopt, how to implement it, and how to migrate legacy code. I've got good news on that front: my first book is about to go to print!

"Modern Angular" is your full guide to everything new in Angular v14–v18, including standalone components, the revamped inputs, Signals (naturally), improved RxJS interoperability, SSR, and more. If that sounds useful, grab it here. The book is currently in copy-editing and will be released soon; right now it's in Early Access with all 10 chapters available online. For updates on the print launch, follow me on Twitter or LinkedIn.

P.S. If you want to get ahead, chapter 5 covers RxJS interoperability, and chapters 6–7 are a deep dive into Signals ;)


Reactive programming in Angular 101 — figure 3

Tagged in:

Articles

Last Update: January 08, 2025

AV
Armen Vardanyan

Writes about RxJS, State, Dependency Injection. Active 2019–2026.

All 57 articles →