Skip to main content

Development

Angular Signals State Management

Angular Signals State Management is a new, built-in approach that simplifies state handling in Angular. Unlike RxJS or NgRx, Signals offer an intuitive, reactive, and efficient way to manage state with automatic dependency tracking and reduced re-renders.

What Are Signals in Angular?

Signals in Angular represent a new reactivity model that allows you to track and update state efficiently. Inspired by reactive programming concepts, Angular Signals State Management leverages automatic dependency tracking to manage state changes without unnecessary re-renders.

Why Use Angular Signals for State Management?

Traditional state management in Angular often involves using RxJS observables, requiring manual subscriptions and unsubscriptions, leading to boilerplate code. Angular Signals simplify this by:

  • Eliminating the need for explicit subscriptions.
  • Automatically tracking dependencies.
  • Reducing re-renders by updating only affected components.

How Signals Work in Angular

Signals are built on a simple concept: reactive values that update automatically when their dependencies change. Let’s explore how to use them in an Angular component.

Defining a Signal in Angular

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import{ signal } from '@angular/core';
exportclass CounterComponent {
count = signal(0);
increment(){
this.count.set(this.count() + 1);
}
}
import { signal } from '@angular/core'; export class CounterComponent { count = signal(0); increment() { this.count.set(this.count() + 1); } }
import { signal } from '@angular/core';

export class CounterComponent {
  count = signal(0);

  increment() {
    this.count.set(this.count() + 1);
  }
}

Here, signal(0) creates a signal with an initial value of 0. The set method updates the signal’s value.

Using Signals in Templates

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
<p>Current Count: {{ count() }}</p>
<button (click)="increment()">Increment</button>
<p>Current Count: {{ count() }}</p> <button (click)="increment()">Increment</button>
<p>Current Count: {{ count() }}</p>
<button (click)="increment()">Increment</button>

The count() function call ensures the template updates reactively whenever the signal changes.

Derived Signals in Angular

Derived signals allow you to create computed values that update automatically.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import{ computed, signal } from '@angular/core';
exportclass CounterComponent {
count = signal(0);
doubleCount = computed(()=>this.count.peek() * 2)
}
import { computed, signal } from '@angular/core'; export class CounterComponent { count = signal(0); doubleCount = computed(() => this.count.peek() * 2) }
import { computed, signal } from '@angular/core';

export class CounterComponent {
  count = signal(0);
  doubleCount = computed(() => this.count.peek() * 2)
}

Now, doubleCount will always be twice the value of count, and updates will be automatic.

Effects with Signals in Angular

Effects enable side effects to run whenever a signal changes.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import{ effect, signal } from '@angular/core';
exportclass LoggerComponent {
count = signal(0);
constructor(){
effect(()=>console.log(`Count has changed to:`, this.count.peek()));
}
}
import { effect, signal } from '@angular/core'; export class LoggerComponent { count = signal(0); constructor() { effect(() => console.log(`Count has changed to:`, this.count.peek())); } }
import { effect, signal } from '@angular/core';

export class LoggerComponent {
  count = signal(0);

  constructor() {
     effect(() => console.log(`Count has changed to:`, this.count.peek()));
  }
}

This effect logs changes to count without manual subscriptions.

Advantages of Using Angular Signals for State Management

  1. Better Performance: Signals update only the affected parts of the UI.
  2. Less Boilerplate: No need for manual subscriptions and unsubscriptions.
  3. Automatic Dependency Tracking: Computed values update automatically.
  4. More Readable and Maintainable Code: The API is simple and easy to use.

When to Use Angular Signals

  • When building reactive UI components.
  • When managing local component state.
  • When reducing reliance on RxJS for simple state updates.

Real-Life Scenarios Showcasing Angular Signals

  Scenario 1: Counter with Derived and Synced State

  Without Signals (Using RxJS)

In a typical RxJS-based approach, syncing a counter and its double value might look like this:

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import{ Component } from '@angular/core';
import{ BehaviorSubject, map } from 'rxjs';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count$ | async }}</p>
<p>Double: {{ doubleCount$ | async }}</p>
<button (click)="increment()">Increment</button>
`,
})
exportclass CounterComponent {
private countSubject = newBehaviorSubject<number>(0);
count$ = this.countSubject.asObservable();
doubleCount$ = this.count$.pipe(map(count => count * 2));
increment(){
const current = this.countSubject.value;
this.countSubject.next(current + 1);
}
}
import { Component } from '@angular/core'; import { BehaviorSubject, map } from 'rxjs'; @Component({ selector: 'app-counter', template: ` <p>Count: {{ count$ | async }}</p> <p>Double: {{ doubleCount$ | async }}</p> <button (click)="increment()">Increment</button> `, }) export class CounterComponent { private countSubject = new BehaviorSubject<number>(0); count$ = this.countSubject.asObservable(); doubleCount$ = this.count$.pipe(map(count => count * 2)); increment() { const current = this.countSubject.value; this.countSubject.next(current + 1); } }
import { Component } from '@angular/core';
import { BehaviorSubject, map } from 'rxjs';

@Component({
  selector: 'app-counter',
  template: `
    <p>Count: {{ count$ | async }}</p>
    <p>Double: {{ doubleCount$ | async }}</p>
    <button (click)="increment()">Increment</button>
  `,
})
export class CounterComponent {
  private countSubject = new BehaviorSubject<number>(0);
  count$ = this.countSubject.asObservable();
  doubleCount$ = this.count$.pipe(map(count => count * 2));

  increment() {
    const current = this.countSubject.value;
    this.countSubject.next(current + 1);
  }
}

 

Disadvantages Without Signals

  • Verbose: Need BehaviorSubject, map, and async pipe.

  • Manual subscription logic required for more complex use-cases.

  • Harder to reason about reactive chains and side effects.

With Angular Signals

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import{ Component, signal, computed } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">Increment</button>
`,
})
exportclass CounterComponent {
count = signal(0);
doubleCount = computed(()=>this.count() * 2);
increment(){
this.count.set(this.count() + 1);
}
}
import { Component, signal, computed } from '@angular/core'; @Component({ selector: 'app-counter', template: ` <p>Count: {{ count() }}</p> <p>Double: {{ doubleCount() }}</p> <button (click)="increment()">Increment</button> `, }) export class CounterComponent { count = signal(0); doubleCount = computed(() => this.count() * 2); increment() { this.count.set(this.count() + 1); } }
import { Component, signal, computed } from '@angular/core';

@Component({
  selector: 'app-counter',
  template: `
    <p>Count: {{ count() }}</p>
    <p>Double: {{ doubleCount() }}</p>
    <button (click)="increment()">Increment</button>
  `,
})
export class CounterComponent {
  count = signal(0);
  doubleCount = computed(() => this.count() * 2);

  increment() {
    this.count.set(this.count() + 1);
  }
}

 

Advantages with Signals

  • Cleaner and more intuitive.

  • computed handles dependencies automatically.

  • No need for manual subscriptions or async pipe.

  • Template automatically re-renders with minimal overhead.

Scenario 2: Form Validation with Side Effects

Let’s say you’re validating a username form field, checking if it’s too short.

Without Signals (RxJS with FormControl)

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import{ Component } from '@angular/core';
import{ FormControl } from '@angular/forms';
@Component({
selector: 'app-username',
template: `
<input [formControl]="usernameControl" />
<p *ngIf="isTooShort">Username is too short</p>
`,
})
exportclass UsernameComponent {
usernameControl = newFormControl('');
isTooShort = false;
constructor(){
this.usernameControl.valueChanges.subscribe(value =>{
this.isTooShort = value.length<5;
});
}
}
import { Component } from '@angular/core'; import { FormControl } from '@angular/forms'; @Component({ selector: 'app-username', template: ` <input [formControl]="usernameControl" /> <p *ngIf="isTooShort">Username is too short</p> `, }) export class UsernameComponent { usernameControl = new FormControl(''); isTooShort = false; constructor() { this.usernameControl.valueChanges.subscribe(value => { this.isTooShort = value.length < 5; }); } }
import { Component } from '@angular/core';
import { FormControl } from '@angular/forms';

@Component({
  selector: 'app-username',
  template: `
    <input [formControl]="usernameControl" />
    <p *ngIf="isTooShort">Username is too short</p>
  `,
})
export class UsernameComponent {
  usernameControl = new FormControl('');
  isTooShort = false;

  constructor() {
    this.usernameControl.valueChanges.subscribe(value => {
      this.isTooShort = value.length < 5;
    });
  }
}


Disadvantages Without Signals

  • Manual subscription to valueChanges.

  • Must remember to unsubscribe to avoid memory leaks.

  • UI logic is split between the template and the component class.

With Angular Signals

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
import{ Component, signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-username',
template: `
<input [value]="username()" (input)="username.set($event.target.value)" />
<p *ngIf="isTooShort()">Username is too short</p>
`,
})
exportclass UsernameComponent {
username = signal('');
isTooShort = computed(()=>this.username().length<5);
constructor(){
effect(()=>{
if(this.isTooShort()){
console.log('Username is invalid');
}
});
}
}
import { Component, signal, computed, effect } from '@angular/core'; @Component({ selector: 'app-username', template: ` <input [value]="username()" (input)="username.set($event.target.value)" /> <p *ngIf="isTooShort()">Username is too short</p> `, }) export class UsernameComponent { username = signal(''); isTooShort = computed(() => this.username().length < 5); constructor() { effect(() => { if (this.isTooShort()) { console.log('Username is invalid'); } }); } }
import { Component, signal, computed, effect } from '@angular/core';

@Component({
  selector: 'app-username',
  template: `
    <input [value]="username()" (input)="username.set($event.target.value)" />
    <p *ngIf="isTooShort()">Username is too short</p>
  `,
})
export class UsernameComponent {
  username = signal('');
  isTooShort = computed(() => this.username().length < 5);

  constructor() {
    effect(() => {
      if (this.isTooShort()) {
        console.log('Username is invalid');
      }
    });
  }
}

 

Advantages with Signals

  • Logic is declarative and reactive.

  • computed and effect automatically update with dependencies.

  • No need for manual subscription/unsubscription.

  • Easier to test and reason about.

Key Takeaways

Feature Without Signals (RxJS) With Signals
Boilerplate High Minimal
Subscription Management Required Not needed
Derived State Manual (map, combineLatest) Automatic with computed
Side Effects Manual (subscribe) Automatic with effect
Template Integration Requires async pipe Direct signal call ()

Conclusion

Angular Signals State Management offers a powerful, efficient, and developer-friendly approach to handling state. By reducing boilerplate and improving reactivity, it simplifies how developers manage state in Angular applications. As Angular continues evolving, Signals may become the preferred method for handling state, making development faster and more intuitive. For a deeper dive into how Signals work in Angular, check out this https://angular.dev/guide/signals

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Ashok Jangid

Ashok Jangid is a Technical Consultant at Perficient, specializing in front-end development with around four years of hands-on experience in Angular. His passion for technology drives his constant curiosity and eagerness to explore new and emerging innovations.

More from this Author

Follow Us