Unveiling the Power of Signals
Signal's Role in Reactivity
In the realm of UI libraries and frameworks such as Angular, React, and Vue, the concept of reactivity plays a pivotal role. Whether it's termed dirty check, diff check vDOM, or change detection, the efficiency of reactivity profoundly impacts a framework's performance. Until Angular v16, zone.js was instrumental in Angular's change detection mechanism. Despite the performance boost brought about by the OnPush strategy, there's always room for further optimization. This is where Signal steps in, offering a nuanced and refined approach to zone.js's change detection mechanism.
Angular, while powerful, often poses a steeper learning curve compared to its counterparts like React and Vue. Additionally, mastering RxJS, an advanced reactive library heavily utilized within Angular, can be daunting for many developers, hindering the adoption of the framework. In response to this challenge, the Angular team endeavors to streamline the framework, and Signal emerges as a compelling solution. It stands out as a user-friendly, high-performance alternative that simplifies handling state changes within Angular applications.
Moreover, Signal's relevance transcends Angular; it finds application within other frameworks such as Solid.js and Preact. By not relying on zone.js, Signal contributes to reduced bundle sizes in Angular applications, enhancing overall performance.
Unlocking the Potential of Signals
- Reactive and Efficient: Signals boast efficient reactivity, crucial for optimal performance.
- Simplicity at its Core: They offer a straightforward approach to managing state changes, reducing complexity in codebases.
- Enhanced Maintainability: Signal-based code tends to be more maintainable, facilitating smoother development workflows.
- Consistency Guaranteed: Unlike observables, signals always carry a value, even if initially undefined, ensuring predictable behavior.
- Minimal Side Effects: Reading a signal doesn't trigger additional changes, promoting cleaner code architecture
Signals vs RxJS
- While RxJS observables excel in emitting multiple values over time, signals maintain a single value, simplifying data display in templates.
- Unlike observables that necessitate asynchronous handling via async pipes, signals promptly retrieve their value, eliminating concerns regarding duplicates and code complexity.
- However, it's worth noting that RxJS remains indispensable for managing asynchronous operations like HTTP requests, with signals primarily serving the purpose of rendering data.
Harmonizing Signals with RxJS
The integration of signals and RxJS is facilitated through @angular/core/rxjs-interop
. By leveraging toSignal
,
observables can be seamlessly converted into signals, with automatic subscription management and value propagation.
While signals and BehaviorSubjects
share similar functionalities, signals offer a more streamlined syntax, albeit with
some additional intricacies during integration with RxJS.
Signal Naming Conventions
While some prefer using the $
prefix convention for signals, no official standard exists yet. As signals gain
widespread adoption, they're expected to gradually replace simple variables entirely, aligning with established variable
naming conventions.
Basic Signal Usage
// WritableSignal
const price = signal(100);
const quantity = signal<number>(1);
// Computed signals are read-only, with memorized values
const total = computed(() => price() * quantity());
console.log(total()); // 100
quantity.set(2); // Set a signal
console.log(total()); // 200
quantity.update((q) => q + 1); // Immutable update
console.log(total()); // 300
effect(() => {
// Triggered whenever quantity changes.
console.log(quantity());
});
effect
Generally, refrain from changing the signal value inside the effect. The effect function executes when one or more dependent signals undergo a change.
When to Use Effect?
- Logging activities
- Utilizing external APIs (not RxJS)
- Typically, not extensively employed
toSignal
Options
vehiclesState = toSignal(this.vehiclesState$, {
initialValue: [],
requireSync: true,
});
-
initialValue
: Specifies the initial value for the signal produced bytoSignal
. This initial value remains until the observable emits its first value. -
requireSync
: Determines whether the observable must emit synchronously upon subscription bytoSignal
. If set to true,toSignal
ensures immediate production of a value upon subscription, eliminating the need to handle undefined in the signal type or provide an initial value. However, this setting may lead to a runtime error if the requirement isn't met.
toObservable
Usage
vehiclesState$ = toObservable(this.vehiclesState);
This function exposes the value of an Angular Signal as an RxJS Observable. Value propagation into the Observable's
subscribers is facilitated through an effect. toObservable
should be invoked in an injection context unless an
injector is supplied via options.
New Features in Angular 17
Signal Queries
export class Menu {
@ViewChild('trigger') trigger: ElementRef | undefined;
@ContentChildren(MenuItem) items: QueryList<MenuItem> | undefined;
}
export class MenuWithSignals {
trigger = viewChild.required('trigger');
items = contentChildren(MenuItem);
}
Signal Inputs
export class Checkbox {
@Input() disabled = false;
@Input({required: true}) checked!: boolean;
}
export class CheckboxWithSignals {
disabled = input(false);
checked = input.required<boolean>();
checked$ = toObservable(this.checked);
}
Evolution of Outputs
Outputs, albeit not signal-based, retain the familiar syntax style. Unlike the conventional @Output
, which extends
EventEmitter
inheriting from Subject
in RxJS, the new output type is OutputEmitterRef
, devoid of any association
with RxJS.
export class CheckboxWithSignals {
disabled = input(false);
@Output() toggled = new EventEmitter<boolean>();
}
import {outputFromObservable, outputToObservable} from '@angular/core/rxjs-interop';
export class CheckboxWithSignals implements OnInit {
disabled = input(false);
checked = input.required<boolean>();
toggled = output<boolean>(); // type OutputEmitterRef<boolean>
toggled$ = outputToObservable(this.toggled);
name$ = new BehaviorSubject('Derek');
changeName = outputFromObservable(this.name$); // OutputRef<string>
handleClick() {
this.toggled.emit(this.checked());
this.name$.next('Iris');
}
ngOnInit() {
// Registers a callback that is invoked whenever the output emits a new value.
// Angular will automatically clean up the subscription
// when the directive/component of the output is destroyed.
this.changeName.subscribe(console.log);
}
}
Model Inputs
In angular 16, it's hard to do a two-way binding using signal
.
@Component({
template: `<input [ngModel]="name()" (ngModelChange)="setName($event)" type="text" name="name" />`,
imports: [FormsModule],
})
export class InputSignalExample {
name = signal('Mike');
setName(value: string) {
this.name.set(value);
}
}
Even without signal, a custom component supporting 2-way binding requires the below syntax. You need a input variable,
and the output variable should be the input variable plus Change
.
@Component({
template: `<input type="checkbox" (change)="handleClick($any($event.target).value)" [checked]="checked" />`,
selector: 'checkbox',
})
export class Checkbox {
@Input() checked = false;
@Output() checkedChange = new EventEmitter<boolean>();
handleClick(value: boolean) {
this.checkedChange.emit(!value);
}
}
// consumer
@Component({
template: `<checkbox [(checked)]="isAdmin" />`,
})
export class Profile {
isAdmin = false;
}
Since model
comes, the syntax is much simplified.
@Component({
template: `<input type="checkbox" (change)="handleClick()" [checked]="checked()" />`,
selector: 'cool-checkbox',
})
export class CoolCheckbox {
checked = model(false);
handleClick() {
this.checked.update((c) => !c);
}
}
// consumer
@Component({
template: `<checkbox [(checked)]="isAdmin" />`,
})
export class Profile {
isAdmin = signal(false);
}
Guidelines for Signal Adoption
- Employ event handlers for user actions.
- Utilize signals or computed values for any potentially mutable data.
- Centralize shared signals within services.
- Continue leveraging Observables for asynchronous operations, especially HTTP requests.
- Employ
toSignal
andtoObservable
for seamless conversions.
Envisioning the Future
In the long run, angular team aims to make RxJS dependency optional. When RxJS is opted for, seamless integration is promised, offering even better integration than ever before.