3d cartoon hands holding a phone

Unlock full course by purchasing a membership

Lesson 5

Coding Reactively/Declaratively in Angular

What exactly does reactive and declarative even mean?

STANDARD

Coding Reactively/Declaratively in Angular

We are going to talk about what reactive/declarative coding in Angular means mostly at a conceptual level in this lesson. But, these definitions and discussions can feel disconnected from the practical side of actually building applications this way. You are going to hear a lot of stuff that might sound… Interesting? Useful? Pointless? But it can be hard to get a sense of what it actually means, what code will you actually write to adhere to these idealistic descriptions.

As we progress through this course, we will be coding and building applications using this philosophy, so hopefully you will be able to build up a strong sense of what it all means as we go. But, to give you some sense of grounding in the practical side of things before we jump too heavily into concepts I will attempt a bare bones definition of what I think reactive/declarative coding actually looks like in Angular at a basic level.

If I had to sum up what reactive/declarative coding in Angular looks like in just a few dot points, I would say:

  • Using observable streams for any data that changes in your application
  • Using operators to combine/transform the data in those streams into the format you need
  • Wherever possible, only subscribing to streams in the template (i.e. with the async pipe)

It’s not a perfect outline, but I think it captures the spirit at a simple level. There isn’t really a concrete definition for what reactive/declarative coding means and people are going to have differing opinions on what you should and should not do (as with just about everything).

NOTE: This lesson has a high level of conceptual complexity. If you are new to these concepts don’t worry if very little of it makes sense. Not knowing this stuff will not prevent you from progressing, it might just affect how you think about the architecture of your applications at a more advanced level. I would recommend just reading through this once, don’t worry if not much of it is making sense, and move on. As you progress, and you have more personal experience/context, it will be worth coming back to this lesson again and reading it multiple times over time.

What does Reactive mean exactly?

At a basic level, we can think of reactivity or reactive programming as reacting to events or changing data. We are expecting something to change, and we define how to handle that change.

Angular is a reactive system. It implements two types of reactivity: transparent and reified. Let’s consider an example:

@Component({
    selector: `app-greeting`,
    template: `
        <h2 *ngIf="name">Hi, {{ name }}!</h2>
    `
})
export class GreetingComponent {
    @Input() name?: string | null;
}

This is a simple component that uses an interpolation bound to the name input. Whenever the name input changes, our template will automatically update to show the correct name. This is transparent reactivity.

The distinguishing feature of this style of reacting to changes is that we only have access to the current latest value or state. We are reacting to whatever the value for name is currently. There is nothing wrong with this, and we will often do this in our applications.

Let’s now consider the parent component that would give this component its input:

@Component({
    selector: `app-home`,
    template: `
        <app-greeting [name]="name"></app-greeting>
    `
})
export class HomeComponent implements OnInit {

    name: string;

    constructor(private userService: UserService){}

    async ngOnInit(){
        this.name = await this.userService.getName();
    }

}

We are now binding [name] in the template to the class member name which is set asynchronously from a service. This is still transparent reactive programming, as we are only dealing with whatever the latest value/state is. If name changes, we will react to the new value.

However, this is the level where we could start benefitting from reified reactivity. The distinguishing feature of reified reactivity is that we don’t work with whatever the current latest value/state is. We work with objects representing the act of observation/change over time rather than the values themselves, i.e. observables.

Angular also implements this style of reified reactivity. Whilst it uses transparent reactivity for propagating state changes as we saw with the name property above, it uses reified reactivity for events. For example, if we have any kind of @Output() on a component that we can react to that event like this:

<app-my-component (itemSelected)="doSomething()" />

This output is implemented using an EventEmitter which is an observable stream of values over time - we have access to the actual object responsible for observing/transforming these values over time (not just whatever the last value was).

This is the way Angular uses reified reactivity, but we can also utilise this concept ourselves with observables. We could refactor our HomeComponent example to use a reified reactive approach:

@Component({
    selector: `app-home`,
    template: `
        <app-greeting [name]="name$ | async"></app-greeting>
    `
})
export class HomeComponent {

    name$ = this.userService.getName();

    constructor(private userService: UserService){}

}

We are now assuming that getName() returns an observable stream that will emit whatever the current name is when subscribed to. Now we don’t just have access to the latest value (as with the transparent approach), in fact we don’t directly have access to any values at all! We have direct access to an object that will be responsible for changing the name value over time, and if we want we can subscribe to this to get the value. This is reified reactivity.

This is a more complex kind of reactivity, although you can probably see that it has actually made our example quite a lot simpler. We don’t need to worry about the ngOnInit lifecycle hook anymore to await our async method from the service, we can just use the name$ stream directly.

With the reified approach, if the name were to be updated at some point (i.e. it was updated in the UserService) then we don’t need to do anything. Our name$ stream will just emit the new value and everything updates automatically.

However, with our example that relies on transparent reactivity, we would need to manually trigger fetching the value from the service and updating it again somehow.

This is not the only benefit that the extra power of reified reactivity offers to us. One additional advantage is the ability to modify streams declaratively with the power that RxJS operators provide us (but we have already talked about that at length), and another is the ability to compose streams together.

Let’s consider another example. This time we are going to add in another service that we will use to retrieve a stream of settings. This stream will be responsible for defining how the name should be displayed (e.g. should we display the full name or just the first name?):

@Component({
    selector: `app-home`,
    template: `
        <app-greeting [name]="name$ | async"></app-greeting>
    `
})
export class HomeComponent {

    name$ = combineLatest([
        this.userService.getName(), 
        this.settingsService.getPreferences()
    ]).pipe(
        map(([name, preferences]) => 
            preferences.fullName ? `${name.first} ${name.last}` : name.first
        )
    )

    constructor(
        private userService: UserService, 
        private settingsService: SettingsService
    ){}

}

We still just have one stream called name$ that emits the name value whenever required. But now we are composing two streams together to generate it - the name stream, and the preferences stream. If either of these streams change, then the value will be recalculated and emitted according to whatever operators we are using.

Doing this without observables is a little more awkward as it requires a lifecycle hook:

@Component({
    selector: `app-home`,
    template: `
        <app-greeting [name]="name"></app-greeting>
    `
})
export class HomeComponent implements OnInit {

    name: string;

    constructor(
        private userService: UserService, 
        private settingsService: SettingsService
    ){}

    async ngOnInit(){
        const [name, preferences] = await Promise.all([
            this.userService.getName(),
            this.userService.getPreferences()
        ]);

        this.name = preferences.fullName ? `${name.first} ${name.last}` : name.first
    }

}

And it still faces the issue that it won’t automatically update if either the name or preferences change in the services.

An important point that I will keep making throughout this course is that when we subscribe to a stream, we are no longer using reified reactivity. If we pull a value out of a stream and manually do something with it we are relying on the transparent reactivity system.

For example, I could modify our example that uses the name$ stream to be this instead:

@Component({
    selector: `app-home`,
    template: `
        <app-greeting [name]="name"></app-greeting>
    `
})
export class HomeComponent implements OnInit {

    name: string;

    constructor(private userService: UserService){}

    ngOnInit(){
        this.userService.getName().subscribe((name) => {
            this.name = name;
        })
    }

}

Just because we are using an observable, it doesn’t mean this is the reified reactive approach that we are generally aiming for. We have taken the value out of the stream, we are no longer dealing with an observable that gives us access to the act of observation/change over time, and this example is effectively no different than our transparent example. It is arguably still a bit better, because the name binding will still be updated automatically if the name is updated in the service, but we lose the rest of the power that the reified approach gives us like composability.

Declarative vs Imperative

We have addressed the reactive part of reactive/declarative, now let’s focus on the declarative part. Similar to the way we can compare transparent and reified, we can also compare declarative to imperative.

The general description of the imperative approach is that it involves coding in a way that says how we want something done. The declarative approach involves coding in a way that says what we want done. But let’s focus on each of these in a little more detail than that.

Let’s focus on what imperative style code is first. The imperative style is how most of us probably think about programming by default. It is a more algorithmic and machine-like way to think about coding. Imagine we have an array:

[1, 2, 3, 4]

As usual, we want to double each element in this array. An imperative approach would look like this - we are defining how to modify each element in the array:

const myArray = [1, 2, 3, 4];

for(let i=0; i < myArray.length; i++){
    myArray[i] = myArray[i] * 2
}

console.log(myArray);

As you can see, this is a more algorithmic/machine-like way to make this change. We can’t look at this example and immediately “read” what is happening at a more human level. We need to think like a computer.

On the other hand, a more declarative style approach would look like this:

const myArray = [1, 2, 3, 4];

const doubledArray = myArray.map((element) => element * 2);

console.log(doubledArray);

NOTE: This is the standard map array method, we are not using RxJS operators here. The declarative approach is a general concept that happens to be embraced by RxJS, it is not unique to RxJS.

This example will give us the same result, but it is much more readable at a human level. We are describing what we want here in our mapping function. We want to multiply each element by 2. We don’t specify how this should happen, the map method will take care of that for us. Under the surface this map method might use a for loop exactly like the one we used in the imperative approach, but that isn’t our concern here.

Why is the declarative approach better than imperative?

It’s not really. In a sense, all code is imperative eventually. Take our map method as an example - it isn’t magic, at some level it still needs to do something imperatively to do its job (i.e. modify the array). The underlying implementation will still loop through the array, and at a low enough level all code becomes 1s and 0s running on circuits which is not at all declarative. However, we can use a much higher level, abstracted, declarative implementation.

The declarative approach generally makes our applications less error prone (it’s easier to mess up a loop then using the map operator), it makes it easier to compose our code (e.g. we can easily chain multiple operators together and even compose streams together), and it also just generally makes it easier to code. Using some abstracted declarative code like startWith(1) is much easier than manually creating a new observable stream that emits 1 and then subscribes to some other observable stream and emits those values.

If we code in a reactive/declarative way, we can utilise the full power of RxJS and its operators. If we don’t, there is going to be a lot more manual handling which will lead to a worse developer experience and likely a buggier application.

Another important example of declarative coding to give for Angular is the usage of components in the template over code in the components class. One good example of this might be creating a modal. Adding this to a components template:

<app-modal></app-modal>

…is very declarative. Alternatively, instead of using a child component in the template, you might create the modal by executing code in your components class to create the modal and inject it into the DOM - this is much more imperative, we are specifying how to create and display the modal. In general, prefer composing components in your templates rather than executing code in your classes.

Dealing with Race Conditions

We are going to discuss one more aspect of coding reactively with RxJS, and that is that (in my opinion at least) it is being honest about the types of challenges/situations we face in our code. RxJS has a lot of operators to deal with highly complex situations - not using those operators doesn’t remove the complexity, we will either need to address that complexity in some other way or just pretend that it doesn’t exist (and build buggy applications).

RxJS and reactive programming explicitly deal with the changing data and race condition environment that a dynamic application naturally creates. The flattening operators that deal with higher-order observables are a good example of this.

There are all these different operators to control exactly how you want to deal with certain race conditions, and with a rather straight-forward syntax. We saw this in the last lesson:

myObservable$.pipe(
    mergeMap(url => this.http.get(url))
).subscribe(val => console.log(val));

The main difficulty with RxJS is in understanding how these operators work in the first place, but once you do they provide an enormous amount of power with a very simple syntax.

It might seem overly complicated, to have so many options. But these situations arise in your application regardless - either you are dealing with them in a more imperative manner and your code will be more complex, or you just don’t account for the race conditions and your application might break/fail under certain conditions randomly.

When dealing with potentially launching multiple HTTP requests over time, how should we approach executing them?

  • Should we cancel existing HTTP requests if a new one is launched?
  • Does everything need to be executed in order?
  • Are some future HTTP requests dependent on previous HTTP requests?
  • Should we prevent launching new HTTP requests until existing ones are finished?

These are the sorts of questions that will apply regardless of whether you are using RxJS operators or not, but RxJS just happens to have a powerful and declarative way to deal with all of these situations explicitly.

Recap

It’s hard to convey the benefits of reactive style programming if you don’t have any experience with the style, but hopefully this lesson has painted somewhat of a picture for you!

What are the two types of reactive systems used in Angular?

What is the defining characteristic of transparent reactivity?

What is the defining characteristic of reified reactivity?

Which of the following is NOT a benefit of using the reified approach with observables?

What is the key difference between imperative and declarative coding?