Master the Art of Reactive State Management in Angular 18+ Without Breaking a Sweat
Introduction
Ever felt like Angular's change detection was that one friend who shows up uninvited to every party? You know, constantly checking if anything changed, even when nothing did? 🤔
Well, Angular Signals just changed the game—and if you're not using computed and linkedSignal yet, you're missing out on some serious performance gains and cleaner code.
By the end of this article, you'll know:
- When to reach for
computedvslinkedSignal(and why it matters) - How to build reactive features without Zone.js breathing down your neck
- Real-world patterns that'll make your senior devs do a double-take
- The performance tricks that separate good Angular apps from great ones
Let's dive in! But first, quick question: Are you still manually managing subscriptions in 2025? Drop a comment below—I'm genuinely curious! 👇
Why Angular Signals Are Your New Best Friend
Remember the days of wrestling with RxJS subscriptions, memory leaks, and that one ngOnDestroy you forgot to implement? Angular Signals are here to save us from that chaos.
Signals bring fine-grained reactivity to Angular—meaning your app only updates what needs updating, when it needs updating. No more Zone.js checking everything under the sun.
// The old way (we've all been there)
export class OldSchoolComponent {
count$ = new BehaviorSubject(0);
doubled$ = this.count$.pipe(map(n => n * 2));
ngOnDestroy() {
// Don't forget this! (But we always do...)
}
}
// The signals way (clean and simple)
export class ModernComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
// No cleanup needed! 🎉
}
Computed Signals: Your Reactive Calculator 🧮
What Are Computed Signals?
Think of computed as that smart friend who always knows the answer because they're constantly doing the math in their head. Computed signals automatically derive values from other signals and update whenever their dependencies change.
The Magic in Action
import { signal, computed } from '@angular/core';
export class PriceCalculatorComponent {
// Base signals
quantity = signal(1);
pricePerUnit = signal(99.99);
discountPercent = signal(0);
// Computed magic happens here
subtotal = computed(() =>
this.quantity() * this.pricePerUnit()
);
discountAmount = computed(() =>
this.subtotal() * (this.discountPercent() / 100)
);
total = computed(() =>
this.subtotal() - this.discountAmount()
);
// Update quantity, everything recalculates automatically!
addToCart() {
this.quantity.update(q => q + 1);
}
}
When should you use computed?
- ✅ Deriving values from other signals
- ✅ Calculations that depend on multiple signals
- ✅ Read-only derived state
- ✅ Performance-critical computations (they're memoized!)
💡 Pro tip: Computed signals are lazy and cached. They only recalculate when accessed AND their dependencies changed. That's free performance right there!
LinkedSignal: The Two-Way Street 🔄
Enter LinkedSignal: Computed's Flexible Cousin
While computed is read-only, linkedSignal is that friend who listens but also has opinions. It can derive from other signals AND be manually updated—perfect for syncing with external sources or handling bi-directional data flow.
LinkedSignal in the Wild
import { signal, linkedSignal } from '@angular/core';
export class UserPreferencesComponent {
// Source signal
fahrenheit = signal(72);
// LinkedSignal that syncs both ways
celsius = linkedSignal(() =>
Math.round((this.fahrenheit() - 32) * 5/9)
);
// You can update it directly!
updateCelsius(value: number) {
this.celsius.set(value);
// Now fahrenheit is out of sync, but that's okay!
// LinkedSignal allows this flexibility
}
// Or sync it back
syncFromCelsius() {
const c = this.celsius();
this.fahrenheit.set(Math.round(c * 9/5 + 32));
}
}
Real-World Example: Form Input Sync
Here's where linkedSignal really shines—syncing form inputs with formatted displays:
export class PaymentFormComponent {
// Raw input value
rawCardNumber = signal('');
// Formatted display that can also be edited
formattedCardNumber = linkedSignal(() => {
const raw = this.rawCardNumber().replace(/\s/g, '');
return raw.match(/.{1,4}/g)?.join(' ') || '';
});
onFormattedInput(value: string) {
// Update the formatted version
this.formattedCardNumber.set(value);
// Extract and update raw
this.rawCardNumber.set(value.replace(/\s/g, ''));
}
}
When to reach for linkedSignal?
- ✅ Two-way data binding scenarios
- ✅ Syncing with external APIs or localStorage
- ✅ Form inputs that need formatting
- ✅ Temporary overrides of computed values
The Ultimate Comparison: Computed vs LinkedSignal 🥊
What They Share
- 🎯 Both derive from other signals
- 🎯 Both update reactively
- 🎯 Both are lazy (compute on-demand)
- 🎯 Both integrate seamlessly with Angular's template system
The Key Differences
| Feature | Computed | LinkedSignal |
|---|---|---|
| Mutability | Read-only | Read-write |
| Use Case | Pure derivations | Bi-directional sync |
| Performance | Highly optimized, cached | Flexible but less cached |
| Best For | Calculations, transformations | Forms, external sync |
| Can be set() | ❌ Never | ✅ Yes |
| Stays in sync | ✅ Always | 🔄 Until manually changed |
Decision Tree (Yes, I Made One for You!)
// Ask yourself:
const shouldUseComputed = () => {
if (needToManuallyUpdate) return false;
if (pureCalculation) return true;
if (externalDataSync) return false;
return true; // When in doubt, computed is usually right
};
Quick question: What's your most complex reactive state scenario? I'd love to hear how you'd solve it with signals! Drop it in the comments 💬
Real-World Example: Dynamic Pricing Calculator 💰
Let's build something practical—a pricing calculator with tax, discounts, and currency conversion:
@Component({
selector: 'app-pricing',
template: `
<div class="pricing-calculator">
<h3>Product Pricing</h3>
<label>
Quantity:
<input type="number"
[value]="quantity()"
(input)="quantity.set(+$event.target.value)">
</label>
<label>
Discount Code:
<input [value]="discountCode()"
(input)="applyDiscount($event.target.value)">
</label>
<div class="results">
<p>Subtotal: {{ subtotal() | currency }}</p>
<p>Discount: -{{ discountAmount() | currency }}</p>
<p>Tax: {{ taxAmount() | currency }}</p>
<h4>Total: {{ finalPrice() | currency }}</h4>
<!-- LinkedSignal for currency display -->
<p>In EUR: € {{ priceInEur() }}</p>
<button (click)="overrideEurPrice()">
Set Custom EUR Price
</button>
</div>
</div>
`
})
export class PricingCalculatorComponent {
// Base signals
quantity = signal(1);
unitPrice = signal(49.99);
discountCode = signal('');
taxRate = signal(0.08); // 8% tax
// Computed for calculations
subtotal = computed(() =>
this.quantity() * this.unitPrice()
);
discountPercent = computed(() => {
const code = this.discountCode();
// Simple discount logic
switch(code) {
case 'SAVE10': return 0.10;
case 'SAVE20': return 0.20;
case 'HALFOFF': return 0.50;
default: return 0;
}
});
discountAmount = computed(() =>
this.subtotal() * this.discountPercent()
);
taxableAmount = computed(() =>
this.subtotal() - this.discountAmount()
);
taxAmount = computed(() =>
this.taxableAmount() * this.taxRate()
);
finalPrice = computed(() =>
this.taxableAmount() + this.taxAmount()
);
// LinkedSignal for currency conversion
// Can be overridden by user or API
priceInEur = linkedSignal(() =>
// Assuming 1 USD = 0.92 EUR
Math.round(this.finalPrice() * 0.92 * 100) / 100
);
applyDiscount(code: string) {
this.discountCode.set(code.toUpperCase());
}
overrideEurPrice() {
// Maybe from an API or user input
const customPrice = prompt('Enter custom EUR price:');
if (customPrice) {
this.priceInEur.set(parseFloat(customPrice));
}
}
}
Unit Testing Your Signals (Because We're Professionals) 🧪
Nobody talks about testing signals, but here's how to do it right:
describe('PricingCalculatorComponent', () => {
let component: PricingCalculatorComponent;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [PricingCalculatorComponent]
});
const fixture = TestBed.createComponent(PricingCalculatorComponent);
component = fixture.componentInstance;
});
it('should calculate subtotal correctly', () => {
component.quantity.set(3);
component.unitPrice.set(10);
expect(component.subtotal()).toBe(30);
});
it('should apply discount code', () => {
component.quantity.set(2);
component.unitPrice.set(50);
component.discountCode.set('SAVE20');
expect(component.discountAmount()).toBe(20);
expect(component.finalPrice()).toBe(86.4); // 80 + 8% tax
});
it('should allow EUR price override with linkedSignal', () => {
component.finalPrice = signal(100); // Mock final price
// Check computed value
expect(component.priceInEur()).toBeCloseTo(92);
// Override it
component.priceInEur.set(95);
expect(component.priceInEur()).toBe(95);
// It's now disconnected from the source
component.finalPrice.set(200);
expect(component.priceInEur()).toBe(95); // Still 95!
});
});
Best Practices & Performance Tips 🚀
Do's ✅
-
Keep computed signals pure
// Good total = computed(() => this.price() * this.quantity()); // Bad - side effects! total = computed(() => { console.log('Computing...'); // Don't do this return this.price() * this.quantity(); }); -
Use linkedSignal for temporary overrides
// Perfect use case autoSavedValue = linkedSignal(() => this.userInput()); // User can override, but it resyncs on source change -
Batch signal updates
updateMultiple() { // Angular batches these automatically! this.firstName.set('John'); this.lastName.set('Doe'); this.age.set(30); // Only one change detection cycle! }
Don'ts ❌
-
Don't create circular dependencies
// This will cause infinite loops! a = computed(() => this.b() + 1); b = computed(() => this.a() - 1); -
Don't overuse linkedSignal
// If you never need to set(), use computed instead readonly = linkedSignal(() => this.source()); // Better: readonly = computed(() => this.source());
💡 Bonus Tip: The Signal Effect Pattern
Here's a pattern I love for side effects with signals:
export class NotificationComponent {
message = signal('');
constructor() {
// React to signal changes with effects
effect(() => {
const msg = this.message();
if (msg) {
this.showToast(msg);
// Auto-clear after 3 seconds
setTimeout(() => this.message.set(''), 3000);
}
});
}
private showToast(msg: string) {
// Your toast logic here
}
}
Recap: Your Signal Superpowers 🦸♂️
Let's wrap this up with what you've learned:
🎯 Computed signals are your go-to for:
- Derived values that are always in sync
- Performance-critical calculations
- Read-only reactive state
🎯 LinkedSignals shine when you need:
- Bi-directional data flow
- Manual overrides of computed values
- Syncing with external sources
🎯 Key takeaway: Start with computed. Only reach for linkedSignal when you actually need that write capability. Your future self (and your team) will thank you.
Let's Keep This Conversation Going! 🚀
💭 What did you think?
Did this clear up the computed vs linkedSignal confusion? What's your take on Angular's new reactive model? Drop a comment below—I read every single one and love the discussions that follow!
👏 Found this helpful?
If this saved you from a signals-induced headache (or taught you something new), smash that clap button! Seriously, even one clap makes my day—and helps other devs discover this content.
🎯 Your Action Items:
- Try this out: Refactor one component to use signals this week
- Spread the knowledge: Share this with that one colleague still using RxJS for everything
One last question before you go:
What Angular topic should I tackle next?
A) Signal-based state management patterns
B) Standalone components deep dive
C) Performance profiling in Angular 18+
Vote in the comments! 👇
🚀 Follow Me for More Angular & Frontend Goodness:
I regularly share hands-on tutorials, clean code tips, scalable frontend architecture, and real-world problem-solving guides.
- 💼 LinkedIn — Let’s connect professionally
- 🎥 Threads — Short-form frontend insights
- 🐦 X (Twitter) — Developer banter + code snippets
- 👥 BlueSky — Stay up to date on frontend trends
- 🌟 GitHub Projects — Explore code in action
- 🌐 Website — Everything in one place
- 📚 Medium Blog — Long-form content and deep-dives
- 💬 Dev Blog — Free Long-form content and deep-dives
- ✉️ Substack — Weekly frontend stories & curated resources
- 🧩 Portfolio — Projects, talks, and recognitions
- ✍️ Hashnode — Developer blog posts & tech discussions
🎉 If you found this article valuable:
- Leave a 👏 Clap
- Drop a 💬 Comment
- Hit 🔔 Follow for more weekly frontend insights
Let’s build cleaner, faster, and smarter web apps — together.
Stay tuned for more Angular tips, patterns, and performance tricks! 🧪🧠🚀
Top comments (0)