I remember the days when managing state in Angular apps meant juggling observables, subjects, and services with a side of zone.js magic.

Then signals came along, and honestly, they’ve made my life so much easier.

Today I want to walk you through Angular signals and show you why I think they’re worth your time.

What Angular Signals Are All About

Think of a signal as a box that holds a value and tells everyone when that value changes. What’s cool is how they automatically keep track of who’s using them and only update what’s needed.

Here’s how you create a basic signal:

import { signal } from '@angular/core';

// Create a signal with an initial value
const count = signal(0);

// Read the signal's value by calling it as a function
console.log('Current count:', count());

Getting Started with Writable Signals

Writable signals are pretty straightforward, you can update them directly. There are two ways to go about it:

import { signal } from '@angular/core';

const counter = signal(0);

// Method 1: Set the value directly
counter.set(5);

// Method 2: Update based on previous value
counter.update((value) => value + 1); // Increments to 6

I reach for update() all the time when working with lists or counters. It’s just cleaner than reading the current value and then setting a new one.

Why Computed Signals Are My Favorite

Here’s where things get really good. Computed signals are basically read-only values that update themselves based on other signals:

import { signal, computed } from '@angular/core';

const price = signal(100);
const taxRate = signal(0.1);
const total = computed(() => price() * (1 + taxRate()));

console.log(total()); // 110

// Change the price, and total just works
price.set(200);
console.log(total()); // 220

The Magic of Lazy Evaluation

One thing I love about computed signals is lazy evaluation. Your code only runs when someone actually needs the value, not when you create it or when its dependencies change.

This saves a ton of processing when you have expensive operations. For example:

const items = signal([1, 2, 3, 4, 5]);
const filteredItems = computed(() => {
  console.log('Filtering items...');
  return items().filter((item) => item % 2 === 0);
});

// Nothing logged yet because filteredItems hasn't been read

// Now the computation runs
console.log(filteredItems()); // Logs "Filtering items..." then [2, 4]

Free Performance with Memoization

The other killer feature is memoization, once your computed signal runs, it remembers the result until something changes:

const userProfile = signal({ name: 'Alice', role: 'Admin' });
const displayName = computed(() => {
  console.log('Computing display name...');
  return `${userProfile().name} (${userProfile().role})`;
});

console.log(displayName()); // Logs "Computing display name..." then "Alice (Admin)"
console.log(displayName()); // Just returns "Alice (Admin)" without computing again

// Change the input, and only then does it recalculate
userProfile.set({ name: 'Bob', role: 'User' });
console.log(displayName()); // Logs "Computing display name..." then "Bob (User)"

This saved my app when I had to filter and sort large lists of data that were accessed repeatedly.

Let’s Build Something Real: A Stock Portfolio Dashboard

Here’s a real-world example of using signals in a stock portfolio dashboard that shows how reactive state management solves common UI challenges:

import { Component } from '@angular/core';
import { signal, computed } from '@angular/core';

interface StockPosition {
  symbol: string;
  shares: number;
  purchasePrice: number;
  currentPrice: number;
}

@Component({
  selector: 'app-portfolio-dashboard',
  template: `
    <div class="portfolio-dashboard">
      <h2>
        Portfolio Dashboard (Total Value: {{ formatCurrency(totalValue()) }})
      </h2>

      <div class="market-status">
        <span
          [class.positive]="marketChange() > 0"
          [class.negative]="marketChange() < 0">
          Market: {{ marketChange() > 0 ? '+' : ''
          }}{{ marketChange().toFixed(2) }}%
        </span>
        <div class="view-controls">
          <button (click)="setView('all')">All Holdings</button>
          <button (click)="setView('gainers')">Gainers</button>
          <button (click)="setView('losers')">Losers</button>
        </div>
      </div>

      <div class="portfolio-metrics">
        <div class="metric">
          <span class="label">Daily P/L:</span>
          <span [class]="dailyProfitLoss() >= 0 ? 'positive' : 'negative'">
            {{ formatCurrency(dailyProfitLoss()) }}
          </span>
        </div>
        <div class="metric">
          <span class="label">Total P/L:</span>
          <span [class]="totalProfitLoss() >= 0 ? 'positive' : 'negative'">
            {{ formatCurrency(totalProfitLoss()) }} ({{
              totalProfitLossPercent().toFixed(2)
            }}%)
          </span>
        </div>
      </div>

      <table class="holdings-table">
        <thead>
          <tr>
            <th>Symbol</th>
            <th>Shares</th>
            <th>Price</th>
            <th>Value</th>
            <th>P/L</th>
            <th>P/L %</th>
          </tr>
        </thead>
        <tbody>
          <tr
            *ngFor="let position of filteredPositions()"
            [class.positive]="getPositionProfitLoss(position) > 0"
            [class.negative]="getPositionProfitLoss(position) < 0">
            <td>{{ position.symbol }}</td>
            <td>{{ position.shares }}</td>
            <td>{{ formatCurrency(position.currentPrice) }}</td>
            <td>
              {{ formatCurrency(position.shares * position.currentPrice) }}
            </td>
            <td>{{ formatCurrency(getPositionProfitLoss(position)) }}</td>
            <td>{{ getPositionProfitLossPercent(position).toFixed(2) }}%</td>
          </tr>
        </tbody>
      </table>

      <div *ngIf="marketOpen()" class="live-updates">
        <span class="pulse"></span> Live updates active
      </div>
    </div>
  `,
})
export class PortfolioDashboardComponent {
  // Writable signals
  marketOpen = signal<boolean>(true);
  marketChange = signal<number>(1.23); // Market index change percentage

  private positions = signal<StockPosition[]>([
    { symbol: 'AAPL', shares: 10, purchasePrice: 150, currentPrice: 175 },
    { symbol: 'MSFT', shares: 5, purchasePrice: 280, currentPrice: 310 },
    { symbol: 'GOOGL', shares: 2, purchasePrice: 2800, currentPrice: 2750 },
    { symbol: 'AMZN', shares: 3, purchasePrice: 3200, currentPrice: 3400 },
    { symbol: 'META', shares: 8, purchasePrice: 330, currentPrice: 290 },
  ]);

  private viewFilter = signal<'all' | 'gainers' | 'losers'>('all');

  // Computed signals for portfolio metrics
  protected totalValue = computed(() => {
    return this.positions().reduce(
      (total, position) => total + position.shares * position.currentPrice,
      0
    );
  });

  protected totalCost = computed(() => {
    return this.positions().reduce(
      (total, position) => total + position.shares * position.purchasePrice,
      0
    );
  });

  protected totalProfitLoss = computed(() => {
    return this.totalValue() - this.totalCost();
  });

  protected totalProfitLossPercent = computed(() => {
    return (this.totalProfitLoss() / this.totalCost()) * 100;
  });

  protected dailyProfitLoss = computed(() => {
    // Simplified calculation assuming market change correlates with portfolio
    return this.totalValue() * (this.marketChange() / 100);
  });

  // Computed signal for filtered positions based on user selection
  protected filteredPositions = computed(() => {
    const currentFilter = this.viewFilter();
    return this.positions().filter((position) => {
      const profitLoss = this.getPositionProfitLoss(position);
      if (currentFilter === 'all') return true;
      if (currentFilter === 'gainers') return profitLoss > 0;
      if (currentFilter === 'losers') return profitLoss < 0;
      return true;
    });
  });

  // Methods for UI interactions
  setView(newView: 'all' | 'gainers' | 'losers') {
    this.viewFilter.set(newView);
  }

  // Utility methods
  getPositionProfitLoss(position: StockPosition): number {
    return position.shares * (position.currentPrice - position.purchasePrice);
  }

  getPositionProfitLossPercent(position: StockPosition): number {
    return (
      ((position.currentPrice - position.purchasePrice) /
        position.purchasePrice) *
      100
    );
  }

  formatCurrency(value: number): string {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
    }).format(value);
  }

  // Simulate real-time price updates
  constructor() {
    setInterval(() => {
      if (!this.marketOpen()) return;

      this.positions.update((positions) =>
        positions.map((position) => ({
          ...position,
          currentPrice:
            position.currentPrice * (1 + (Math.random() * 0.01 - 0.005)),
        }))
      );

      // Update market trend occasionally
      if (Math.random() > 0.8) {
        this.marketChange.update(
          (change) => change + (Math.random() * 0.2 - 0.1)
        );
      }
    }, 1000);
  }
}

The cool thing about this setup? I don’t need to write a bunch of code to keep everything in sync:

  1. When stock prices bounce around, all the metrics just update on their own
  2. Switch to view only gainers? The table filters right away without any extra work
  3. And with those price updates happening every second, I’d have gone crazy trying to manually recalculate everything before signals came along

How Signals Stack Up Against Alternatives

SignalsRxJS ObservablesNgRx
Learning curveLowModerate to HighHigh
Boilerplate codeMinimalSomeExtensive
DebuggingSimple (plain JS values)Can be complexDevTools available
IntegrationBuilt into AngularRequires operators knowledgeNeeds additional packages

Wrapping Up

I used to write a ton of boilerplate just to keep my UI in sync with my data. With signals, that’s mostly gone. The lazy evaluation and memoization have fixed performance issues I didn’t even know I had, especially in data-heavy pages.

For me, signals work best in components that need to display derived data, things that depend on other things.

Before, I’d have to manually sync everything whenever anything changed. Now the computed signals just handle it.

If you haven’t tried them yet, give signals a shot in your next project. They’ve made my Angular code simpler and more predictable, and that’s always a win in my book.

Frequently Asked Questions

What are Angular Signals?

Angular Signals are reactive primitives that hold values and notify dependents when those values change. They help track who’s using them and only update what’s needed, making state management more efficient and predictable.

How do writable signals work in Angular?

Writable signals hold values you can update in two ways: using set() to directly assign a new value (like counter.set(5)), or update() to modify based on the previous value (like counter.update(value => value + 1)).

What are computed signals in Angular?

Computed signals are read-only values that automatically update based on other signals. They calculate derived data and recalculate only when their dependency signals change, keeping your UI in sync with your data without manual intervention.

What is lazy evaluation in Angular Signals?

Lazy evaluation means computed signals only run their calculation code when someone actually needs the value - not when created or when dependencies change. This saves processing power, especially for expensive operations.

How does memoization work with Angular Signals?

Memoization in Angular Signals means computed values are remembered until dependencies change. Once calculated, the result is cached and returned for subsequent reads without recalculating, which greatly improves performance.

How do Angular Signals compare to RxJS Observables?

Angular Signals are simpler and more focused than RxJS Observables. Signals have a lower learning curve, minimal boilerplate, and are easier to debug since they work with plain JavaScript values. Observables are more powerful for complex async operations but require deeper knowledge.

Can I use Signals for component state management?

Yes! Signals are perfect for component state management. They’re simpler than services with BehaviorSubjects while offering automatic UI updates when values change. For local UI state like filters, sorting, or toggle states, signals are often the ideal solution.

What’s the benefit of signals over NgRx?

Signals have much less boilerplate than NgRx, making them ideal for small to medium apps. They’re built directly into Angular, don’t require additional packages, and have a gentler learning curve. NgRx still makes sense for large apps with complex state.

Do signals replace RxJS in Angular?

No, signals don’t fully replace RxJS. They work well for simpler state management cases, but RxJS still excels at complex async operations, event streams, and combining multiple data sources. Many apps use both technologies where each makes the most sense.

How do I get started using signals in my Angular app?

To get started with signals, import the signal and computed functions from @angular/core. Create a signal with signal(initialValue) and read its current value by calling it as a function. For derived values, use computed(() => calculation using other signals).
See other angular posts