Angular Signals in Enterprise Apps: Patterns That Actually Scale
After migrating three large enterprise codebases to Angular Signals, Ive identified the patterns that work under pressure — and the anti-patterns that will haunt you in production.
Why Signals Change Everything
After 20 years of Angular development, I've seen the framework evolve dramatically — from AngularJS to Zone.js-driven change detection to the reactive patterns we use today. But Signals represent the most significant shift in how Angular applications manage state since the introduction of RxJS.
I've now migrated three large enterprise codebases to Signals. Here's what I learned.
The Core Concept in 30 Seconds
A Signal is a reactive value that Angular tracks automatically:
import { signal, computed, effect } from '@angular/core';
// A writable signal
const count = signal(0);
// A computed signal — derived automatically
const doubled = computed(() => count() * 2);
// An effect — runs when signals it reads change
effect(() => {
console.log(`Count is ${count()}, doubled is ${doubled()}`);
});
// Update the signal
count.set(5); // effect runs: "Count is 5, doubled is 10"
No subscriptions. No manual unsubscribe(). No async pipe in templates for simple state.
Pattern 1: Component-Level State
Replace local BehaviorSubject with Signals for component state. The before/after is striking:
- private loading$ = new BehaviorSubject<boolean>(false);
- loading$ = this.loading$.asObservable();
+ loading = signal(false);
- this.loading$.next(true);
+ this.loading.set(true);
In your template:
<!-- Before -->
<spinner *ngIf="loading$ | async" />
<!-- After — no async pipe needed -->
<spinner *ngIf="loading()" />
Pattern 2: Computed State in Services
This is where Signals shine in enterprise apps. Instead of combining multiple observables with combineLatest, use computed:
@Injectable({ providedIn: 'root' })
export class CartService {
private items = signal<CartItem[]>([]);
private discount = signal<number>(0);
// Automatically updates when items or discount changes
total = computed(() => {
const subtotal = this.items().reduce((sum, item) => sum + item.price * item.qty, 0);
return subtotal * (1 - this.discount());
});
itemCount = computed(() => this.items().length);
addItem(item: CartItem) {
this.items.update(current => [...current, item]);
}
}
The
computedsignal is lazy — it only recalculates when one of its dependencies actually changes and something reads it.
Pattern 3: Mixing Signals with RxJS
You don't have to choose. Use toSignal and toObservable as bridges:
import { toSignal, toObservable } from '@angular/core/rxjs-interop';
@Component({ standalone: true })
export class SearchComponent {
searchTerm = signal('');
// Convert signal to Observable to use HTTP client
results = toSignal(
toObservable(this.searchTerm).pipe(
debounceTime(300),
switchMap(term => this.http.get<Result[]>(`/api/search?q=${term}`))
),
{ initialValue: [] }
);
}
Anti-Pattern: Overusing Effects
This is the most common mistake I see in code reviews:
// ❌ DON'T DO THIS
effect(() => {
this.totalPrice.set(
this.items().reduce((sum, item) => sum + item.price, 0)
);
});
// ✅ DO THIS INSTEAD
totalPrice = computed(() =>
this.items().reduce((sum, item) => sum + item.price, 0)
);
Effects should be reserved for side effects (logging, DOM manipulation, calling external APIs). Derived state belongs in computed.
Performance: OnPush + Signals
Signals work beautifully with OnPush change detection. Angular knows exactly which components to update:
@Component({
changeDetection: ChangeDetectionStrategy.OnPush,
template: `<h1>{{ title() }}</h1>`
})
export class MyComponent {
title = signal('Hello');
}
When title changes, Angular only re-renders this component — not the entire tree.
Conclusion
Signals are production-ready and the Angular team has made it clear they're the future of reactivity in the framework. Start migrating component-level state first, then services, and use toSignal/toObservable as bridges where RxJS is essential (HTTP, complex async flows).
The patterns above have held up across 200k+ LOC codebases. Start small, measure your performance improvements, and let the results speak for themselves.
Comments