Published on

Angular Signals

Authors
  • avatar
    Name
    Iraianbu A
    Twitter

Table of Contents

Introduction

A signal is a essentially a wrapper around a value that notifies consumers (functions,components or services) when the value changes. This allows for a reactive data flow, where components and services can subscribe to signals and react to changes in the value.

Signals in Angular are a way to manage state and reactive data flows, similar to Observables or BehaviorSubjects. Signals are more lightweight and built directly into the framework, making them easier to use and understand.

Characteristics

  1. Hold any type of value (Primitive, Object, Array, etc.)
  2. Exposes a getter function to read the current value.
  3. Notifies Angular when their value is used, allowing Angular to track dependencies automatically.

Types of Signals

  1. Writable Signals (Can be updated)
  2. Read-only Signals (Cannot be updated)

Writable Signals

const count = signal<number>(0)
count.set(1)
count.update((count) => count + 1)
  • set - Sets the value of the signal directly
  • update - Updates the value of the signal using a callback function

Read-only Signals

const count: Signal<number> = signal<number>(0)
console.log(count())

Computed Signals

Computed signals are read-only signals that are derived from other signals.

Computed Signals Example

Computed signals are useful when you need to derive a value from another signal and you want to update the derived value when the original signal changes.

  • Lazy Evaluation (Computed signals are only evaluated when the value is used)

  • Memoization (Computed signals are memoized and only re-evaluate when the dependencies change)

Dynamic Computed Signals

const showCount = signal(false)
const count = signal(0)
const conditionalCount = computed(() => {
  if (showCount()) {
    return `The count is ${count()}.`
  } else {
    return 'Nothing to see here!'
  }
})

When showCount is false, conditionalCount returns "Nothing to see here!" without reading count. Updating count won't trigger recomputation.

When showCount is true, conditionalCount reads count and shows its value. Now updating count will trigger recomputation.

Dependencies can be added or removed dynamically. Setting showCount back to false removes count as a dependency.

Reading Signals in OnPush Components

@Component({
  selector: 'app-my-component',
  template: `{{ count() }}`,
  changeDetection: ChangeDetectionStrategy.OnPush,
})

In OnPush components, Angular automatically tracks signals used in templates and updates the component when signal values change.

Usecases

  1. Expensive Calculations
  2. Data Transformation
  3. Read-only data

Key Benefits

  1. Simple State Management (Provides an easy way to manage state rather than using Observables and Subscriptions)

  2. Reactive Data Flow (Angular automatically tracks dependencies and updates components when the signal value changes)

  3. Type Safety (Signals are typed and provide type safety)

Effects

Effects allows us to perform side effects like API calls, logging, etc. when one or more signals change.

It is a function that run whenever one or more signals change. When we create an effect, Angular tracks the signals used within that effect and re-runs the effect when the signals change.

const count = signal(0)
effect(() => {
  console.log(count())
})

Effects Example

Working

Initial execution : Effects always run once when the component is created.

Tracking dependencies : Effects track the signals used within them.

Asynchronous execution : Effects execute asynchronously during Angular's change detection cycle.

Usecases

  1. Logging data
  2. Keeping in sync with window.localStorage
  3. Adding Custom DOM behavior

Avoid effects when

Avoid using effect for state propagation (i.e. when a signal is used to update another signal). This can result in infinte circular dependencies, ExpressionChangedAfterItHasBeenChecked.

const count = signal(0)
const doubleCount = signal(1)

effect(() => {
  doubleCount.set(count() * 2)
})

Injection Context

We can create an effect() within an injection context like constructor or inject.

@Component({...})
export class SignalComponent {
  count = signal(0);
  constructor() {
    effect(() => {
      console.log(this.count());
    });
  }
}

We can assign the effect to a field.

@Component({...})
export class SignalComponent {
  count = signal(0);
  private loggingEffect = effect(() => {
    console.log(this.count());
  });
}

To create an effect outside the constructor, we can pass an Injector to the effect function.

@Component({...})
export class EffectiveCounterComponent {
  readonly count = signal(0);
  private injector = inject(Injector);
  initializeLogging(): void {
    effect(() => {
      console.log(`The count is: ${this.count()}`);
    }, {injector: this.injector});
  }
}

Destroying Effects

When we create an effect, it's automatically destroyed when the component is destroyed. Effect return an EffectRef object which has a destroy method. We can also manually destroy an effect by passing {manualCleanup: true} to the effect function.

@Component({...})
export class SignalComponent {
  count = signal(0);
  private loggingEffect = effect(() => {
    console.log(this.count());
  } , {manualCleanup: true});

  ngOnDestroy(): void {
    this.loggingEffect.destroy();
  }
}

References

Angular Signals