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:
- When stock prices bounce around, all the metrics just update on their own
- Switch to view only gainers? The table filters right away without any extra work
- 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
Signals | RxJS Observables | NgRx | |
---|---|---|---|
Learning curve | Low | Moderate to High | High |
Boilerplate code | Minimal | Some | Extensive |
Debugging | Simple (plain JS values) | Can be complex | DevTools available |
Integration | Built into Angular | Requires operators knowledge | Needs 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.