Angular Signals Explained Like a Senior Developer (Angular 21 Perspective)

Published: (February 24, 2026 at 12:36 PM EST)
4 min read
Source: Dev.to

Source: Dev.to

TL;DR — Signals were introduced to eliminate accidental complexity.

The Problem with Traditional RxJS in UI State

In many enterprise Angular applications, teams encounter issues such as:

  • Nested switchMap chains
  • BehaviorSubject misuse
  • Subscription leaks
  • Race conditions in UI state

RxJS was often used to solve problems it wasn’t designed for, turning simple UI state into unreadable streams and increasing production risk.

What Signals Aim to Solve

Signals target a specific class of pain: local, synchronous, UI‑driven state that never required a stream. They are:

  • Reactive value containers
  • Read synchronously (signal())
  • Updated explicitly (set, update)
  • Automatically dependency‑tracked

Core API

import { signal, computed, effect } from '@angular/core';

const count = signal(0);

count.set(1);
count.update(v => v + 1);

const double = computed(() => count() * 2);

effect(() => {
  console.log('Count changed:', count());
});

No subscriptions are needed—signals are deterministic state containers.

Old Model vs. Signal Model

Traditional RxJSSignals
Who subscribed to what?Which value depends on which signal?
Manual subscription management, explicit unsubscriptionAngular tracks dependency reads at runtime
Global change detectionFine‑grained updates, predictable recomputation, reduced unnecessary change detection

Typical Use Cases

  • Load user data
  • Show loading indicators
  • Control permissions
  • Derive UI flags

Example: User Store

// user.store.ts
import { signal, computed, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';

export interface User {
  id: string;
  name: string;
  permissions: string[];
}

export class UserStore {
  private http = inject(HttpClient);

  readonly user = signal(null);
  readonly loading = signal(false);

  readonly canEdit = computed(() =>
    this.user()?.permissions.includes('EDIT_PROFILE') ?? false
  );

  loadUser() {
    this.loading.set(true);

    this.http.get('/api/user').subscribe({
      next: user => this.user.set(user),
      complete: () => this.loading.set(false)
    });
  }
}

Absent: BehaviorSubject, manual unsubscription, async‑pipe complexity. Derived state is expressed with computed.

Example: Profile Component

import { Component } from '@angular/core';
import { UserStore } from './user.store';

@Component({
  standalone: true,
  selector: 'app-profile',
  providers: [UserStore],
  template: `
    @if (store.loading()) {
      
    }

    @if (store.user()) {
      
## {{ store.user()!.name }}

      @if (store.canEdit()) {
        Edit Profile
      }
    }
  `
})
export class ProfileComponent {
  constructor(public store: UserStore) {
    this.store.loadUser();
  }
}

No async pipe is required.

Benefits of Signals

  • No memory leaks
  • Easy refactoring
  • Smaller cognitive load
  • Cleaner tests

Test Example

it('should compute permission correctly', () => {
  const store = new UserStore();
  store.user.set({ id: '1', name: 'A', permissions: ['EDIT_PROFILE'] });

  expect(store.canEdit()).toBe(true);
});

Pure state test—no TestBed needed.

When Not to Use Signals

Signals are not ideal for:

  • WebSocket streams
  • Event orchestration
  • Complex asynchronous flows
  • Cancellation logic

These scenarios remain the domain of RxJS.

Effects for Side Effects

effect(() => {
  if (store.user()) {
    analytics.track('user_loaded');
  }
});

effect() should handle side effects only; use computed for derivations.

Angular Version Timeline

  • Angular 16 – Introduced signals
  • Angular 17‑18 – Template integration, control‑flow blocks
  • Angular 20‑21 – Stable mental model, seamless interoperability

Signals are now architectural primitives, allowing Angular to:

  • Track exact dependencies
  • Avoid tree‑wide dirty checking
  • Minimize recomputation boundaries

In zoneless apps, this becomes transformative.

Why Were Signals Introduced?

To reduce accidental complexity in UI state and enable deterministic, fine‑grained reactivity.

When to Choose RxJS

When modeling asynchronous event streams or complex orchestration that requires cancellation, buffering, or multicasting.

Practical Guidelines

  • One store per feature
  • Use signals for UI/local state
  • Use RxJS for async orchestration
  • Prefer computed over manual state wiring
  • Keep signals close to their usage
  • Keep effect() logic‑light (side effects only)

Balancing signals and RxJS creates maintainable, predictable applications.

Conclusion

Signals restore clarity to Angular’s state management. Use them where they fit, and Angular 21 will feel predictable again.

0 views
Back to Blog

Related posts

Read more »