Skip to content

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:

Play

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.