Angular Signals Explained Like a Senior Developer (Angular 21 Perspective)
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
switchMapchains BehaviorSubjectmisuse- 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 RxJS | Signals |
|---|---|
| Who subscribed to what? | Which value depends on which signal? |
| Manual subscription management, explicit unsubscription | Angular tracks dependency reads at runtime |
| Global change detection | Fine‑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
computedover 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.