How Angular Signals Made State Management So Much Easier
Angular Signals are fantastic. State management got so much easier. I used Redux and NgRx before, and honestly they were a pain. Since I started using Signals, the way I think about state management changed fundamentally.
Freed from the concept of time
I was freed from the RxJS mindset of when state changes. For example, code like this:
// Older syntax (RxJS)
const counter$ = new BehaviorSubject(0);
counter$.pipe(
debounceTime(300),
distinctUntilChanged()
).subscribe(value => {
// Handle value changes
});
// Current syntax (Signals)
const counter = signal(0);
effect(() => {
// Automatically runs whenever the counter value changes
console.log(counter());
});
With RxJS I had to think about when state changed; with Signals I only need to think about what the state is. That is a huge relief.
Freed from the hassle of state management
When I used Redux or NgRx, I wrote a lot of boilerplate—actions, reducers, selectors. With Signals I can write something this simple:
// Basic Signals example
const count = signal(0);
const doubledCount = computed(() => count() * 2);
const isEven = computed(() => count() % 2 === 0);
// Update state
count.update(value => value + 1);
The "management" part of state management is automated by Signals. As a developer, I only need to define what the state is.
Practical example: a counter app
Here is what the code can look like:
import { Component } from '@angular/core';
import { signal, computed, effect } from '@angular/core';
@Component({
selector: 'app-counter',
template: `
<div>
<h2>Counter: {{ count() }}</h2>
<p>Double: {{ doubledCount() }}</p>
<p>{{ isEven() ? 'Even' : 'Odd' }}</p>
<button (click)="increment()">Increment</button>
<button (click)="decrement()">Decrement</button>
<button (click)="reset()">Reset</button>
</div>
`,
})
export class CounterComponent {
// Source data
count = signal(0);
// Derived data
doubledCount = computed(() => this.count() * 2);
isEven = computed(() => this.count() % 2 === 0);
// Side effect
constructor() {
effect(() => {
console.log(`Count changed: ${this.count()}`);
});
}
// Actions
increment() {
this.count.update(value => value + 1);
}
decrement() {
this.count.update(value => value - 1);
}
reset() {
this.count.set(0);
}
}
This is very simple. Less state-management code means more focus on business logic.
One-way data flow
With Signals, data flows in one direction. For example:
// Source data (the foundation of application state)
const sourceData = signal({ user: null, items: [] });
// Derived data
const user = computed(() => sourceData().user);
const items = computed(() => sourceData().items);
const hasItems = computed(() => items().length > 0);
// UI state
const isLoading = signal(false);
const selectedItemId = signal(null);
const selectedItem = computed(() => {
const id = selectedItemId();
return id ? items().find(item => item.id === id) : null;
});
Debugging got much easier. The data flow is clear, so I can see quickly where a problem occurs.
Summary
After using Signals, these points stood out:
- Freed from the concept of time — No more RxJS when; cleaner code
- Freed from state-management hassle — No more Redux-style boilerplate
- Simple data pipelines — Express state as a network of Signals
- One-way data flow — Easier debugging
- Relationships as functions — UI as a function of state; state as a function of data
For small to medium applications, Signals alone seem enough without pulling in a large library like NgRx. I plan to keep pushing Signals hard.
See you next time.