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.

Why are we even bothering with this nonsense? We should just build things the obvious way and get the job done. We’re here to build apps, not write pretty code!

These are all common thoughts about declarative code, and I will not blame you for having the same thoughts yourself.

The trouble with teaching the benefits of reactive and declarative code is that for most people it is not as intuitive as the alternative (imperative code) in the beginning. The benefits of declarative code also only really become apparent when the bigger picture is taken into account — reactive/declarative code rarely looks beneficial when looking at isolated examples. It is only when you consider the application in a more broad context that the benefits become apparent.

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.

By this point, we already have some sense of what reactive means… by I keep using these terms together: reactive/declarative. I would argue that writing declarative code is actually the most important aspect here, reactive code and patterns just facilitate writing declarative code.

There are different ways we can think about the definition of declarative code, some of which we will talk about in this lesson, but I will now give you what I think is the most direct and simple definition of how we will be coding declaratively.

This is a declaration:

comments = ['hi', 'hello', 'how are you?']

We have declared a class member called comments. What is comments? We can answer that just by looking at its declaration. It is an array containing three strings: hi, hello, and how are you.

We have declarative code with no need for reactive mechanisms like observables or signals.

But what if we wanted to create another declaration that contained only the last comment in the array? No problem:

comments = ['hi', 'hello', 'how are you?']
lastComment = comments[comments.length - 1];

We are still being completely declarative here. What is lastComment? Just by looking at the declaration of lastComment I know exactly what it is — it has the value of whatever the last item in the comments array is. I also know exactly what the comments array is just by looking at its declaration. This is all declarative.

But this makes the assumption that this data will never change. What happens if we do this:

comments = ['hi', 'hello', 'how are you?']
lastComment = comments[comments.length - 1];

someMethod(){
  this.comments.push('uh oh');
}

Now everything has fallen apart. Can I look at comments and know what it is just by looking at its declaration? No. Not anymore. I know what it is initially, but to understand its full behaviour over time I need to find anywhere else in the code where it is being imperatively changed. Our someMethod imperatively changes what comments is.

There is also a problem with lastComment now. It’s value is only ever set to what comments was initially — when comments is updated, lastComment is not updated. We need to do that manually… or imperatively:

comments = ['hi', 'hello', 'how are you?']
lastComment = comments[comments.length - 1];

someMethod(){
  this.comments.push('uh oh');
  this.lastComment = this.comments[this.comments.length - 1];
}

We are already getting a preview of the benefits of declarative code — the imperative code above is super prone to bugs. If we forget to manually update lastComment it will not have the correct value.

We can fix this with signals and make it more declarative:

comments = signal(['hi', 'hello', 'how are you?'])
lastComment = computed(() => this.comments()[this.comments().length - 1])

someMethod(){
  this.comments.update((comments) => ([...comments, 'a bit better']))
}

This is still not completely declarative — we are still imperatively updating comments. How can we tell this is imperative? Because we don’t really know what comments is unless we also find and understand what someMethod is doing. We can’t just look at the declaration of comments.

But, our lastComment is declarative and now it will be automatically updated when comments updates without us needing to do it manually.

You will see as we progress that we do this sort of “cheating” quite a lot. An Angular application can not really be “fully” declarative. Often we will have some small imperative operations happening, and then have everything after that point be reactive/declarative.

But wait a minute! Isn’t this a module about observables? Signals can help us be declarative for situations dealing with synchronous reactivity which is what the above example is — we don’t need to wait for anything to complete.

But observables are what can help us be declarative when dealing with asynchronous reactivity. Let’s jump right to a more advanced example to highlight the power of RxJS and observables.

We might have some declarative code like this:

articlesForPage$ = this.currentPage$.pipe(
  startWith(1),
  switchMap((page) =>
    this.apiService.getArticlesByPage(page).pipe(
      retry(5)
    )
  )
)

firstArticle$ = this.articlesForPage$.pipe(
  map((articles) => articles[0])
)

Let’s apply our declarative test — can we understand what articlesForPage$ is just by looking at its declaration? It is much more complex now, but yes. It takes whatever the currentPage$ is and makes a request to the API to load the data for that specific page. If currentPage$ has not emitted any values yet it will start with a page number of 1 by default, and if the request fails it will retry the request 5 times before giving up.

The end result is that articlesForPage$ is the articles for the current page, and we can see exactly how it is calculated. There is no manual handling or setting of the articlesForPage$ data outside of its declaration — everything is right there in a nice little box.

This is what we need observables for. We could use signals for a similar situation, but it would not be declarative because signals can not handle asynchronous reactivity. We would have to execute the HTTP request and then imperatively update a signal. We would also need to imperatively handle dealing with the retries as well.

The same goes for firstArticle$ — everything that it is and ever will be is defined right there in the declaration. Since firstArticle$ does not deal with any asynchronous operations, this one could technically be a signal if we wanted it to be:

articlesForPage$ = this.currentPage$.pipe(
  startWith(1),
  switchMap((page) =>
    this.apiService.getArticlesByPage(page).pipe(
      retry(5)
    )
  )
)

articlesForPage = toSignal(this.articlesForPage$);

firstArticle = computed(() => {
  this.articlesForPage()[0];
})

We are going to talk a lot more about when/how to combine signals and observables together in the state management lesson.

It is this combining of multiple declarative values together where the real power is. We can just have these long interconnected chains of everything just automatically reacting to the things it depends on changing — no need to worry about imperative updates or forgetting to manually refresh some value that you didn’t realise depends on the thing you just updated.

The above is really the most important take away from this lesson — in the rest of the lesson we are going to cover a bit more about the concept of “reactivity” in more general and perhaps more esoteric terms.

What does Reactive mean exactly?

Different people have a lot of different interpretations of what “reactive” means.

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 — even when we are not using signals or observables. It implements two types of reactivity: transparent and reified. Let’s consider an example:

@Component({
    standalone: true,
    selector: `app-greeting`,
    template: `
        @if(name){
          <h2>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({
    standalone: true,
    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({
    standalone: true,
    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({
    standalone: true,
    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.

Recap

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 the best description for what makes something declarative