DEV Community

Cover image for LinkedSignal vs Computed: The Angular Signals Showdown You've Been Waiting For
Rajat
Rajat

Posted on

LinkedSignal vs Computed: The Angular Signals Showdown You've Been Waiting For

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 computed vs linkedSignal (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! 🎉
}

Enter fullscreen mode Exit fullscreen mode

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);
  }
}

Enter fullscreen mode Exit fullscreen mode

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));
  }
}

Enter fullscreen mode Exit fullscreen mode

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, ''));
  }
}

Enter fullscreen mode Exit fullscreen mode

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
};

Enter fullscreen mode Exit fullscreen mode

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));
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

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!
  });
});

Enter fullscreen mode Exit fullscreen mode

Best Practices & Performance Tips 🚀

Do's ✅

  1. 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();
    });
    
    
  2. Use linkedSignal for temporary overrides

    // Perfect use case
    autoSavedValue = linkedSignal(() => this.userInput());
    // User can override, but it resyncs on source change
    
    
  3. 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 ❌

  1. Don't create circular dependencies

    // This will cause infinite loops!
    a = computed(() => this.b() + 1);
    b = computed(() => this.a() - 1);
    
    
  2. 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
  }
}

Enter fullscreen mode Exit fullscreen mode

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:

  1. Try this out: Refactor one component to use signals this week
  2. 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! 🧪🧠🚀

✨ Share Your Thoughts To 📣 Set Your Notification Preference

Top comments (0)