← All articles

How Angular Signals Made State Management So Much Easier

Why Angular Signals freed me from RxJS timing and NgRx boilerplate—simpler state, one-way data flow, and a practical counter example.

Published
How Angular Signals Made State Management So Much Easier cover image

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:

  1. Freed from the concept of time — No more RxJS when; cleaner code
  2. Freed from state-management hassle — No more Redux-style boilerplate
  3. Simple data pipelines — Express state as a network of Signals
  4. One-way data flow — Easier debugging
  5. 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.