Essential Angular Signals Cheatsheet: Boost Your Daily Productivity
This is a full guide about Angular Signals. You can check it everytime you need something about Signals. Take it as your daily check list because soon or later your company will tell you to migrate.
Interfaces / Types
Signal (readonly)
Every reactive value on Angular at the end will extend or it’s of this type (Signal).
(() => T)
: this part says that a Signal
is a function that takes no arguments and returns a value of type T
. This is how you get the current value from a Signal
.
💡 Do you know why Angular uses a Symbol on the Signal<T> type?
Because it will be easier and unique to identify reactive values.
So, every time you create a signal
it will be of this type: Signal<T>
. And here is how we can create a signal
:
// Signal<number>
// without asReadonly() it would be a WritableSignal<number>
now = signal(Date.now()).asReadonly();
WritableSignal
A signal with a value that can be mutated with set()
or update()
. We can make it also readonly
with asReadonly()
.
I use a lot the asReadonly()
feature in my Signal Stores. I explain this store in this two videos I made in my YouTube channel:
NG0103: Infinite change detection while refreshing application views
export class AppComponent {
count = signal(5);
constructor() {
afterRender(() => {
// Don't use update ❌
this.count.update(x => x + 1);
// Use set ✔️
this.count.set(x => x + 1);
})
}
}
Be very careful with using update()
within the afterRender()
hook as we would enter an infinite loop of recalculations. For this it is better to use set()
.
ReactiveNode
Inside the graph of reactivity all nodes
(consumers/producers
) are of this type, ReactiveNode
. Why we need this interface? Because it will let the graph
when to update depending on version
and other properties.
export const REACTIVE_NODE: ReactiveNode = {
version: 0 as Version,
lastCleanEpoch: 0 as Version,
dirty: false,
producerNode: undefined,
producerLastReadVersion: undefined,
producerIndexOfThis: undefined,
nextProducerIndex: 0,
liveConsumerNode: undefined,
liveConsumerIndexOfThis: undefined,
consumerAllowSignalWrites: false,
consumerIsAlwaysLive: false,
producerMustRecompute: () => false,
producerRecomputeValue: () => {},
consumerMarkedDirty: () => {},
consumerOnSignalRead: () => {},
};
Reactivity System in Angular
Angular signals were released on version 16 and become stable on version 17 (effect()
is still in developer preview).
Create Readonly Signals
You can specify that some argument from some function expect a Signal type, make it readonly with the asReadonly()
or creating a computed()
signal:
search = signal(''); // WritableSignal<string>
search2 = this.search.asReadonly(); // Signal<string>
// Signal<string>
uppercaseSearchTerm = computed(() => this.search().toUpperCase());
getSomething(expect: Signal<string>): void {
// arg is of type readonly signal
}
Create Writable Signals
Just by using the signal()
function from the core
package:
search = signal(''); // WritableSignal<string>
Read Signals
You just have to invoke them because at the end they are just wrappers of values such as functions:
// app.component.ts
search = signal('');
searchText = `Search: ${search()}`
// app.component.html
<p>search()</p>
Computed
Computed
allows us to listen to Signals
and when they change do recomputations. Computed
signals are readonly.
A basic example would be:
export class AppComponent {
counter = signal(0); // 🔢 Initial counter value
// 🧮 Computed value that depends on counter
incrementCounterWith2 = computed(() => this.counter() + 2);
constructor() {
// 📊 Demonstrating reactivity of computed values
console.log('📈 Counter incremented:', this.incrementCounterWith2()); // 2
this.counter.set(2);
console.log('📈 Counter incremented:', this.incrementCounterWith2()); // 4
}
}
Here we can see that incrementCOunterWith2
dependes on the first signal
(counter) and when counter changes, it recalculates incrementCOunterWith2
.
NG0600: Writting to signals is not allowed in a `computed` or an `effect`by default.
export class AppComponent {
counter = signal(0);
// 🚫 Problematic computed value
incrementCounterWith2 = computed(() => {
this.counter.set(5); // 🛑 Error: NG0600
return this.counter() + 2;
});
}
If we take a look into the above code we can’t set signal
new values inside computed()
or we will get the following error:
Computed() optional parameter: equal()
You can choose when to recalculate your signal
in this way:
incrementCounterWith2 = computed(() => this.counter() + 2, {
equal: (a, b) => a === b
});
It means that if a === a
it wont be recalculated.
WeakRef deprecated in favour of StrongRef
Imagine we have the following code:
let a = computed(() => {
let b = computed(() => this.counter());
return true;
});
a
with b
have a StrongRef
but counter()
with a has a WeakRef
and this is a problem with the Garbage Collector
because sometimes a won’t be recalculated. The Angular team fixed this here.
Effects (@developerPreview)
With signals
we can have hooks to be listening to our signals
by just using the Effect API.
We must define them in an injection context or using the runInInjectionContext()
function:
ngOnInit(): void {
this.runInInjectionContext(this.injector, () => {
effect(() => {
console.log('Effect works inside ngOnInit()');
});
});
}
Prevent Error: NG0600 error seen in computed()
We can use it’s optional parameter allowSignalWrites:
export class AppComponent implements OnInit {
example = signal(2);
data = input.required<number[]>();
injector = inject(Injector);
ngOnInit(): void {
runInInjectionContext(this.injector, () => {
effect(() => {
console.log('Hijo', this.example() + this.data().length);
this.example.set(5); // Works
}, {
allowSignalWrites: true
});
});
}
}
With this parameter we will be able to write signals inside effect
. Note that this is not posible inside computed()
. But be careffuly with this and what are you writting to, to avoid infinite loops.
Effect manualCleanup
You can use this optional parameter in effect
to manually clean it.
fieldInitializerEffect = effect(
() => console.log('Field initializer', this.todoList()),
{ manualCleanup: true }
);
And then you can do:
this.fieldInitializerEffect.destroy();
Another way to destroy/clean the effect
is with the buildIn method onCleanup
:
constructor() {
effect(onCleanup => {
console.log('Constructor', this.todoList());
// imagine we have this:
// sub... a = Subcription
onCleanup(() => {
// a.unsubscribe();
});
});
}
And all the options we can pass to the effect
are those:
injector?: Injector
manualCleanup?: boolean
allowSignalWrites?: boolean
Untracked
It’s very helpful when we do not want to listen on some signal
changes:
export class AppComponent {
counter1 = signal(1);
counter2 = signal(2);
suma = computed(() => this.counter1() + this.counter2());
constructor() {
effect(() => {
console.log(
`Sum: ${this.counter1() + untracked(() => this.counter2())}`
);
});
}
}
In this example, our effect
won’t run if counter2()
change, just when counter1()
does.
Signal Based Inputs (17.1) - @developerPreview
We can convert our traditional inputs (@Input()
) with signals:
export class AppComponent {
books = input<Book[], Book[]>([], {
alias: '',
transform: (data: Book[]) => data
});
}
As you can see if we use our input()
with transform, our first generic
is what we receive on our input
and the second one is what we return from our transform function
.
Here are a list of the input()
options that can receive:
alias?: string
transform?: (v: TransformT) => T
You can also make them required
:
books = input.required<Book[], Book[]>([]);
Two-way data binding with Model Input (17.2) - @developerPreview
@Input() / @Output()
Angular uses the @Input()
and @Output()
decorators along with event binding to achieve two-way data binding between parent and child components. This is often referred to as the "banana in a box" syntax.
@Component({
selector: 'app-parent',
standalone: true,
imports: [ChildComponent],
template: `
<app-child [(value)]="parentValue" />
<p>Parent value: {{ parentValue }}</p>
`
})
export class ParentComponent {
parentValue = '';
}
@Component({
selector: 'app-child',
standalone: true,
template: `
<input [value]="value" (input)="onInputChange($event)" />
`
})
export class ChildComponent {
@Input() value = '';
@Output() valueChange = new EventEmitter<string>();
onInputChange(event: Event) {
const newValue = (event.target as HTMLInputElement).value;
this.valueChange.emit(newValue);
}
}
ngModel
NgModel
is another way to implement two-way data binding in Angular, often used with template-driven forms. It's a simpler syntax compared to the Input/Output method, but it's important to note that NgModel is specific to form controls.
<input [(ngModel)]="value" (ngModelChange)="valueChange.emit($event)" />
New Model Input
Angular 17.2 introduced a new way to handle two-way data binding
using the model()
function. This approach simplifies the process and provides a more integrated solution within Angular's reactivity system.
@Component({
selector: 'app-parent',
standalone: true,
imports: [FormsModule, ChildComponent],
template: `
<h2>Parent Component</h2>
<input [(ngModel)]="parentValue">
<p>Parent value: {{ parentValue }}</p>
<app-child [(value)]="parentValue"></app-child>
`
})
export class ParentComponent {
parentValue = 'Initial value';
}
@Component({
selector: 'app-child',
standalone: true,
imports: [FormsModule],
template: `
<h3>Child Component</h3>
<input [ngModel]="value()" (ngModelChange)="value.set($event)">
<p>Child value: {{ value() }}</p>
`
})
export class ChildComponent {
value = model('');
}
Under the hood it uses the OutputEmitterRef
class. Also, you can play with this new function using set()
, update()
or subscribe()
:
// app.component.ts
data = model();
data.subscribe();
data.set();
data.update();
You can also use different syntax in the template:
<app [(data)] />
<app [data] />
<app (dataChange) />
The only one available optional parameter is to pass an alias:
// optional
data = model<Data>({ alias: 'currentData' });
// required
data = model.required<Data>({ alias: 'currentData' });
// then when you use it:
<app [currentData]="data" />
Signal Queries (17.2) - @developerPreview
They are an incredible improvement over the loading of views. Why? Because we no longer have to use a component lifecycle like afterViewInit
or afterContentInit
to wait for our query to resolve. With the signals
you do it on the fly.
So, instead of using the decorator based query, use the signal one:
@ViewChild() → viewChild()
@ViewChildren → viewChildren()
@ContentChild → contentChild()
@ContentChildren → contentChildren()
Output API (17.3) - @developerPreview
The new output API
(17.3) eliminates the need to use RxJS
and does it manually relying on OutputEmitterRef
and OutputRef
. In addition, it does the unsubscribe cleanup
for us.
@Directive({
// ...
})
export class MyDir {
nameChange = output<string>();
onClick = output();
}
Use the emit()
method on the OutputEmitterRef
to send values:
updateName(newName: string): void {
this.nameChange.emit(newName);
}
And in templates you can use it this way:
<my-component (nameChange)="handleNameChange($event)" />
Parent components can subscribe to the output
using OutputRef#subscribe
.
You can also pass an optional option to it (alias):
alias = output<Data>({ alias: 'aliasEvent' });
// then when you use it
<component (aliasEvent)="handleData($event)" />
@angular/core/rxjs-interop package
The Angular team has created some features in this package to make RxJS and Signals coexistence pleasant.
toSignal() - @developerPreview
I used this one so much when I have a request that returns an Observable
and I want it to be a signal
:
// favourites.service.ts
private _favouritesSubject$ = new BehaviorSubject<Photo[]>([]);
getFavourites(): Observable<Photo[]> {
return this._favouritesSubject$.asObservable();
}
// favourites.component.ts
favourites = toSignal(
this._favouritesService.getFavourites()
);
toObservable() - @developerPreview
It’s useful when for example you want to get an Observable
from a Signal
. Look at example:
// search.component.html
search = signal('');
searchTerm = outputFromObservable(
toObservable(this.search).pipe(
debounceTime(500),
distinctUntilChanged()
)
);
Here I use toObservable()
because I need to generate an output()
and this output()
needs an Observable
. Look at this superpower!
outputFromObservable() - @developerPreview
When you want to generate an output()
from an Observable
. Check the previous example.
outputToObservable() - @developerPreview
When you want to generate an Observable
from an output()
. Imagine you receive some value from you child component and you want to convert it to Observable
to be able to use some async pipe
or to pass to something that expects an Observable
.
// app.component.ts
data = output();
handleData(): void {
const data$ = outputToObservable(this.data);
// do whatever you want with this data$ (Observable)
// ...
}
Assertions
A reactive context
in Angular is an execution environment where the framework is actively tracking dependencies and setting up subscriptions for reactive primitives
like signals
or observables
.
For example, we are in a reactive context when are are inside the body of effect()
or computed()
, etc.
So, when we declare our toSignal()
, effect()
or the afterRender()
hook we must ensure that we are outside this reactive context.
Why? To be able to infinite loops or unexpected behavior. For that we have this assertion:
export function assertNotInReactiveContext(
debugFn: Function,
extraContext?: string
): void {
if (getActiveConsumer() !== null) {
throw new RuntimeError(
RuntimeErrorCode.ASSERTION_NOT_INSIDE_REACTIVE_CONTEXT,
ngDevMode &&
`${debugFn.name}() cannot be called from within a reactive context.${
extraContext ? ` ${extraContext}` : ''
}`,
);
}
}
Graph - Consumers Producers
Every signal, every node that fills the graph of reactivity
in Angular is a producer or a consumer or both.
Producer: we produce new values (when you create a signal or update it’s value).
Consumer: you consume the value produced.
Mixed: this happens with
computed
, but why? Because you wait for it (consume) and then you do another calculation (producer).
Signal utilities - ngxtensions
You have a detailed list of utilities you can use from ngxtensions. I will focus on derivedAsync
because is the one that I most use:
With derivedAsync
you can react to a signal value and then perform something. Let me put the examples that is on the documentation of this utility:
export class MovieCard {
movieId = input.required<string>();
movie = derivedAsync(() =>
fetch(`https://localhost/api/movies/${this.movieId()}`).then((r) =>
r.json(),
),
);
}
Look at the power of this, if the input changes, the previous request will be canceled and the movie signal
willl be recalculated. You can find more details on the documentation.
Summary
The reactivity system in Angular is very rich and I made this cheatsheet about signals to clarify the graph.
Thanks for reading so far 🙏
I’d like to have your feedback so please leave a comment, clap or follow. 👏
Spread the Angular love! 💜
If you really liked it, share it among your community, tech bros and whoever you want! 🚀👥
Don't forget to follow me and stay updated: 📱
Thanks for being part of this Angular journey! 👋😁