Building Scalable Angular Applications with Signals
Building Scalable Angular Applications with Signals
Angular Signals represent a paradigm shift in how we think about reactivity in Angular applications. Let's explore how to leverage them for building scalable, performant applications.
What Are Signals?
Signals are a reactive primitive that holds a value and notifies consumers when that value changes. Unlike RxJS Observables, signals are synchronous and always have a current value.
import { signal, computed, effect } from "@angular/core";
// Create a signal
const count = signal(0);
// Read the value
console.log(count()); // 0
// Update the value
count.set(5);
count.update((c) => c + 1);
Why Signals Over RxJS?
| Feature | Signals | RxJS |
|---|---|---|
| Learning Curve | Low | High |
| Synchronous | Yes | No |
| Always has value | Yes | No |
| Memory footprint | Small | Larger |
| Best for | UI State | Async operations |
Computed Signals
Derived state becomes trivial with computed signals:
@Component({
selector: "app-cart",
template: `
<div class="cart">
<p>Items: {{ itemCount() }}</p>
<p>Total: {{ formattedTotal() }}</p>
</div>
`,
})
export class CartComponent {
private items = signal<CartItem[]>([]);
protected itemCount = computed(() => this.items().length);
protected total = computed(() => this.items().reduce((sum, item) => sum + item.price, 0));
protected formattedTotal = computed(() => `$${this.total().toFixed(2)}`);
}
Effects for Side Effects
When you need to perform side effects based on signal changes:
export class UserService {
private user = signal<User | null>(null);
constructor() {
// Automatically runs when user changes
effect(() => {
const currentUser = this.user();
if (currentUser) {
analytics.track("user_logged_in", { id: currentUser.id });
}
});
}
}
Best Practices
1. Keep Signals Private
export class CounterComponent {
// Private signal
private _count = signal(0);
// Public read-only access
readonly count = this._count.asReadonly();
increment() {
this._count.update((c) => c + 1);
}
}
2. Use computed() for Derived State
Never compute values in templates—always use computed():
// ❌ Bad: Computation in template
template: `<p>{{ items().filter(i => i.active).length }}</p>`;
// ✅ Good: Use computed
activeCount = computed(() => this.items().filter((i) => i.active).length);
template: `<p>{{ activeCount() }}</p>`;
3. Avoid Nested Signals
// ❌ Bad: Signal of signals
const data = signal(signal({ name: "test" }));
// ✅ Good: Flat structure
const data = signal({ name: "test" });
Integration with OnPush
Signals work seamlessly with OnPush change detection:
@Component({
selector: "app-optimized",
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<h1>{{ title() }}</h1>
<p>Count: {{ count() }}</p>
`,
})
export class OptimizedComponent {
title = input.required<string>();
count = signal(0);
}
Conclusion
Signals simplify state management in Angular applications while improving performance. Start using them today for:
- Local component state
- Derived/computed values
- Simple inter-component communication
For complex async operations, RxJS remains the better choice. The key is knowing when to use each tool.
Ready to try signals in your project? Check out the official Angular documentation for more details.