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.
Again, these videos are not a required part of this course, but they may help build additional context around what “declarative” actually means. The first focuses on the “why” of declarative code:
and the second focuses on how we can adhere to the fundamental idea behind coding declaratively — not reassigning values after they have been declared — but still have our application actually do things.
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({
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({
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.
Recap
What are the two types of reactive systems used in Angular?
Incorrect
Correct!
Incorrect
What is the defining characteristic of transparent reactivity?
Correct!
Incorrect
Incorrect
What is the defining characteristic of reified reactivity?
Incorrect
Incorrect. This is somewhat true, the EventEmitter is used in the reified approach, but it is not its defining characteristic
Correct!
Which of the following is the best description for what makes something declarative
Incorrect
Correct!
Incorrect