Back to overview

Unveiling the Power of Signals

Angular

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 by toSignal. This initial value remains until the observable emits its first value.

  • requireSync: Determines whether the observable must emit synchronously upon subscription by toSignal. 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 and toObservable 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.