An Overview of Signal APIs in Angular
We’ve already looked at the basic primitive building blocks of signals in
Angular which are: signal, computed, and effect.
However, we are not just given these bare bones primitives to make use of ourselves. Angular is also further integrating these into Angular itself and exposing core functionality via signal APIs.
input
We have already seen one example of this: input. In the case of inputs, we
don’t need to actually create any signals ourselves. We just use the input
function provided by Angular to define an input, and then we get that input
given to us as a signal within the component.
This is particularly useful in the case of inputs, because we could then use other signal primitives in combination with that.
For example, if we want to derive some other value from an input:
name = input.required<string>();reversedName = computed(() => [...this.name()].reverse().join(""))Now we take the value of name and create a new computed signal that reverses
the name. The cool thing is that whenever name changes our reversedName will
update automatically.
What if we wanted to run some side effect when name changed? We can easily do
that too:
name = input.required<string>();
constructor(){ effect(() => { console.log(this.name()) })}Now every time the name input changes, our effect function will run.
View Queries
We will be diving more into the specifics of view queries as we use them in the
applications we will build, but this is another story very similar to the
migration from @Input to input.
In some cases, we might want to grab a reference to some element in our template.
In a previous lesson, we saw that we could create a template variable like this:
<p #myParagraph></p>If we wanted to grab a reference to that in our class we could use a “view child” to do that. Previously, with the old decorator based approach, that would have looked like this:
@ViewChild('myParagraph') myParagraph;With the new signal based API it looks like this:
myParagraph = viewChild('myParagraph');Again, it is the same story as input where myParagraph is now provided as a
signal which means we would access its value like this: this.myParagraph().
Also, just as with inputs, we can do things like create derived values from this
signal using computed or run side effects with effect.
As well as viewChild we also have viewChildren for retrieving multiple
references, and then we also have contentChild and contentChildren for
situations where we are dealing with “projected” content. We are going to make
use of this a little later, but the general idea with “projected” content is
that rather than having some content directly in the template of a component
like this:
<div> <p>I am directly in the template, I'm a view child!</p></div>We might want to supply content dynamically to a component — this is sort of
like supplying an input but it’s part of the template. To do this, our child
component would use ng-content like this:
<div> <ng-content></ng-content></div>Now there is no p tag directly in the template; it is not a “view child”. But
now, in the parent component, we can dynamically pass in this part of the
template:
<app-my-comp-with-content-projection> <p>I am passed in from the parent, I will be a content child!</p></app-my-comp-with-content-projection>The end result in the DOM will be the same, e.g:
<div> <p>I am passed in from the parent, I will be a content child!</p></div>Everything from within the opening/closing tags of the component in the parent
component gets “projected” into the child component wherever the ng-content
tag is placed.
model
A model looks deceptively like an input. One basic use case it enables is that
it allows a component that defines an “input” to also modify the value of that
input.
For example, in this case:
import { Component, input } from '@angular/core';
@Component({ selector: 'app-welcome', template: ` <p>Hi, {{ name() }}!</p> `,})export class WelcomeComponent { name = input('friend');}The name input can only be changed by passing the value in through the parent
component:
<app-welcome [name]="user.name" />If we wanted to set the value of name from within the WelcomeComponent
we would not be allowed to do that as an input signal is read-only:
import { Component, input } from '@angular/core';
@Component({ selector: 'app-welcome', template: ` <p>Hi, {{ name() }}!</p> `,})export class WelcomeComponent { name = input('friend');
resetName(){ // does NOT work this.name.set(''); }}However, model will allow us to do that:
import { Component, model } from '@angular/core';
@Component({ selector: 'app-welcome', template: ` <p>Hi, {{ name() }}!</p> `,})export class WelcomeComponent { name = model('friend');
resetName(){ // does work this.name.set(''); }}Another pattern that model facilitates is two-way data binding. Perhaps
the easiest way to think about a model in relation to an input in this way
is that a model provides a way to “share” a signal between a parent and child
component.
Consider a normal input:
<app-welcome [name]="user.name" />Here, we pass the value of user.name in as the input. This is a string. Within
the app-welcome component that string value will be provided as a signal
that returns a string value when we access its value, e.g: this.name()
As we have seen, a model can be defined like this:
import { Component, model } from '@angular/core';
@Component({ selector: 'app-welcome', template: ` <p>Hi, {{ name() }}!</p> `,})export class WelcomeComponent { name = model('friend');}But instead of supplying a simple string input, we can supply a signal like this:
import { Component, model } from '@angular/core';
@Component({ selector: 'app-parent-component', template: `<app-welcome [(name)]="name" />`,})export class WelcomeComponent { name = signal('josh');}Notice that rather than passing in something like a string that then gets
turned into a signal, we actually create and supply the signal ourselves.
Another thing to notice is that we are using the “banana-in-a-box” syntax for
binding name: [(name)].
We are going to dive more into this syntax later when we discuss ngModel but,
in brief, you can think of it this way: we use [] to bind inputs that pass
data in to a component, and we use () to bind outputs/events that pass data
out of a component. This [()] syntax means we both want values going in
and we also want to receive values back out via the same mechanism: the name
signal being supplied as the model input.
This means that within the parent compnent we could call set on the signal to
alter its value, and the child component would be able to react to that value
change. Likewise, we could also call set on the signal from within the child
component and we would be able to react to this change in the parent component.
This is the basic idea of two-way data binding: data can flow both up and down through the same mechanism.
We’ve already discussed handling two way data flow with inputs and outputs; that
is a way to achieve two way data flow with two different mechanisms. With
model we can achieve this two-way data flow within just the one mechanism.
The concept of two-way data binding sort of runs counter to the declarative ideas we will be focusing on in this course. It is slightly early to dive into the specifics of this now, but generally a declarative approach will rely more on the idea of “uni-directional”, or “one-way” data flow.
That isn’t to say that model will never be useful (custom form controls are a
particularly relevant use case for model), but we will not generally be
relying on it in this course.
resource
Signals typically work best in the world of synchronous changes. You click a button, a value updates immediately: that’s synchronous.
But, the recent introduction of the resource API allows us to more easily
perform asynchronous operations with signals. You click a button, that
triggers a request to an API, a value updates once the request finishes: that’s
asynchronous. There is waiting involved, and your code goes on executing
something else whilst that waiting happens.
Wanting to fetch some data from an API and set that data in a signal is a very
common use case. Before the resource API, without bringing RxJS into the
equation, we could only really handle this situation with imperative code that
uses an effect. That might look something like this:
page = signal(1);results = signal([]);
constructor(){ effect(() => { const page = this.page(); fetch(`https://example.com/${page}/`) .then((res) => this.results.set(res.json())); })}We have an effect that will run when the value of the page signal changes.
We use that value to launch a request, and when the request completes we set a
signal with the returned value from within the effect.
If you’re not familiar with the difference between imperative code and declarative code yet: don’t worry. I’m not expecting that you are. This is going to be a major focus of the course as we progress.
If you would like a little extra context on why “not using effects” is often recommended here is a supplementary video:
The resource API allows us to do something like this instead:
page = signal(1);results = resource({ params: this.page, loader: ({params, abortSignal}) => { return fetch(`https://example.com/${params}/`, { signal: abortSignal }).then((res) => res.json()); }});No effects, this code is declarative, and this code also provides us with an
abortSignal that we can supply to the fetch request. This means that if the
page changes whilst a request is still in progress, the underlying request
will be cancelled.
There are more added bonuses here as well. By using the resource API not only
do we get our results available as a signal:
this.results.value()We also get signals that tell us the current status of the resource - if it is currently loading for example - and if there were any errors:
this.results.status()this.results.error()These sorts of things are non-trivial to set up, but they are typically
necessary in real world scenarios. So, the resource API provides a huge
benefit here by giving it to us for free.
An important thing to note here is that, at least at the moment, it is far more
common to see the observable based HttpClient used for HTTP requests in
Angular rather than the Promise based fetch API. The example above shows how
a request can be executed declaratively without observables, but the resource
API also supports using observables and the HttpClient for requests via the
use of rxResource:
import { rxResource } from "@angular/core/rxjs-interop";
http = inject(HttpClient);
page = signal(1);results = rxResource({ params: this.page, stream: (request) => this.http.get(`https://example.com/${params}/`)});We will be relying heavily on resource and rxResource in this course.
linkedSignal
The linkedSignal seems like a bit of a weird one, because on the surface it
seems like it’s kind of the same thing as computed.
For example:
count = signal(1);doubleCount = computed(() => this.count() * 2)count = signal(1);doubleCount = linkedSignal({ source: this.count, computation: (source) => source * 2});What is the difference here? Apart from the fact that linkedSignal looks more
complicated to use. Both result in a derived signal where the value is doubled.
The key difference is that linkedSignal returns a WritableSignal whereas
computed returns a readonly Signal. This means that if we use a
linkedSignal we can manually update the resulting signal, but it will still
automatically be recomputed every time the signals it depends on change:
count = signal(1);doubleCount = linkedSignal({ source: this.count, computation: (source) => source * 2});
increment(){ // increment the doubled count this.doubleCount.update((doubleCount) => doubleCount + 1)}With this example we could keep incrementing the doubleCount by calling the
increment method. But, as soon as count changes, the values we manually set
will be overwritten by the computation running again.
I feel like this can all seem a little abstract without a realistic example. We
will get into the details of this in the first major application we build in the
course, but this is a linkedSignal example from that application:
checklistItems = linkedSignal({ source: this.loadedChecklistItems.value, computation: (checklistItems) => checklistItems ?? [], });Our source comes from a resource that is loading some value from storage.
Initially this value will be undefined and so we just return an empty array.
Once it returns a value from storage, we will use that value instead. And,
unlike with computed, we still have the ability to add more data to this
checklistsItems manually after it has loaded.
There are more differences, and more powerful applications that linkedSignal
has. We will get into some of those as we progress through the course.
Summary
The integration of signals into Angular is an ongoing project and we are likely to continue seeing more signal based APIs in future releases of Angular.