Back to all posts
Angular Signals Performance

Building Scalable Angular Applications with Signals

12 min read

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.