DEV Community

Cover image for Angular Signals Effect(): Why 90% of Developers Use It Wrong
Rajat
Rajat

Posted on

Angular Signals Effect(): Why 90% of Developers Use It Wrong

Master Angular's New Reactivity System Without the Performance Pitfalls Most Devs Face


Have you ever felt like Angular's change detection was working against you instead of with you? πŸ€”

If you've been wrestling with zone.js, fighting unnecessary re-renders, or pulling your hair out over ExpressionChangedAfterItHasBeenCheckedError, you're not alone. Angular Signals are here to save the dayβ€”but there's a catch. The very tool that makes signals powerful, the effect() function, is also the easiest way to shoot yourself in the foot.

By the end of this article, you'll learn:

  • What Angular Signals really are (beyond the hype)
  • How to use effects properly without tanking your app's performance
  • The 5 most common effect() mistakes that even senior devs make
  • Battle-tested patterns for clean, scalable reactive code

Let's dive in! But first, quick question for you πŸ‘‡

πŸ’¬ What's your biggest pain point with Angular's current change detection? Drop a commentβ€”I'm genuinely curious what battles you're fighting!


Introduction to Angular Signals: The Game Changer We've Been Waiting For

Remember the days of manually calling detectChanges() or wrestling with OnPush strategies? Angular Signals are the reactive primitive we've been asking for since, well, forever.

Think of signals as smart containers for your state. Unlike regular variables that Angular has to constantly check for changes, signals announce when they've changed. It's like having a friend who actually texts you when they're running late instead of making you guess.

Here's your first taste of signals in action:

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

export class ShoppingCartComponent {
  // Create a signal with initial value
  itemCount = signal(0);

  // Computed signals automatically update when dependencies change
  cartMessage = computed(() => {
    const count = this.itemCount();
    return count === 0
      ? 'Your cart is empty πŸ›’'
      : `You have ${count} item${count > 1 ? 's' : ''} in your cart`;
  });

  addItem() {
    // Update the signal - all computed values update automatically!
    this.itemCount.update(count => count + 1);
  }
}

Enter fullscreen mode Exit fullscreen mode

Pretty clean, right? But here's where things get interesting...


Understanding Effects in Angular Signals: Your New Best Friend (Used Wisely)

Effects are where signals meet the real world. They're functions that run automatically whenever their signal dependencies change. Think of them as the bridge between your reactive state and side effects like API calls, logging, or localStorage updates.

When Should You Actually Use Effects?

Effects are perfect for:

  • Syncing with external systems (localStorage, WebSockets, analytics)
  • Debugging and logging state changes
  • Bridge operations between signals and non-signal code

Here's a real-world example that makes sense:

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

export class UserPreferencesService {
  theme = signal<'light' | 'dark'>('light');

  constructor() {
    // βœ… GOOD: Syncing with localStorage
    effect(() => {
      const currentTheme = this.theme();
      localStorage.setItem('user-theme', currentTheme);
      document.body.className = currentTheme;

      // This is a perfect use case - external side effect!
      console.log(`Theme changed to: ${currentTheme}`);
    });
  }

  toggleTheme() {
    this.theme.update(t => t === 'light' ? 'dark' : 'light');
  }
}

Enter fullscreen mode Exit fullscreen mode

Quick check: Are you using effects for the right reasons? Let's find out... πŸ‘‡


Common Misuses of Effects: The Mistakes That'll Haunt Your Codebase

Alright, here's where we get real. I've reviewed hundreds of Angular codebases, and these effect() mistakes keep showing up like bad pennies. Let's break them down.

❌ Mistake #1: Using Effects for Derived State

This is the big one. I see this everywhere:

// 🚫 DON'T DO THIS
export class ProductComponent {
  price = signal(100);
  discount = signal(0.2);
  finalPrice = signal(0);

  constructor() {
    effect(() => {
      // This is computing derived state - use computed() instead!
      const calculated = this.price() * (1 - this.discount());
      this.finalPrice.set(calculated);
    });
  }
}

// βœ… DO THIS INSTEAD
export class ProductComponent {
  price = signal(100);
  discount = signal(0.2);

  // Computed automatically tracks dependencies
  finalPrice = computed(() =>
    this.price() * (1 - this.discount())
  );
}

Enter fullscreen mode Exit fullscreen mode

Why it matters: Effects run asynchronously and can cause timing issues. Computed signals are synchronous and more performant.

❌ Mistake #2: The Infinite Loop Trap

Ever created an infinite loop by accident? With effects, it's easier than you think:

// 🚫 DANGER ZONE
export class CounterComponent {
  count = signal(0);
  doubledCount = signal(0);

  constructor() {
    effect(() => {
      // Reading and writing in the same effect = infinite loop!
      if (this.count() < 10) {
        this.count.update(c => c + 1); // πŸ’₯ BOOM!
      }
    });
  }
}

Enter fullscreen mode Exit fullscreen mode

❌ Mistake #3: Nesting Effects (The Performance Killer)

// 🚫 PERFORMANCE NIGHTMARE
effect(() => {
  const userId = this.currentUser();

  effect(() => {  // Nested effect - creates memory leaks!
    const preferences = this.userPreferences();
    // This inner effect never gets cleaned up...
  });
});

// βœ… BETTER APPROACH
effect(() => {
  const userId = this.currentUser();
  const preferences = this.userPreferences();
  // Handle both in a single effect
});

Enter fullscreen mode Exit fullscreen mode

πŸ’¬ Have you fallen into any of these traps? Which one bit you the hardest? Share your war stories in the comments!

❌ Mistake #4: Effects for API Calls (Without Cleanup)

// 🚫 PROBLEMATIC
effect(() => {
  const searchTerm = this.searchInput();

  // This fires on EVERY keystroke!
  this.http.get(`/api/search?q=${searchTerm}`)
    .subscribe(results => {
      this.searchResults.set(results);
    });
});

// βœ… PROPER IMPLEMENTATION
private searchEffect = effect((onCleanup) => {
  const searchTerm = this.searchInput();

  // Debounce the search
  const timer = setTimeout(() => {
    const sub = this.http.get(`/api/search?q=${searchTerm}`)
      .subscribe(results => {
        this.searchResults.set(results);
      });

    // Cleanup subscription
    onCleanup(() => sub.unsubscribe());
  }, 300);

  // Cleanup timeout
  onCleanup(() => clearTimeout(timer));
});

Enter fullscreen mode Exit fullscreen mode

❌ Mistake #5: Using Effects for Component Communication

// 🚫 ANTI-PATTERN
export class ParentComponent {
  parentSignal = signal('');

  constructor() {
    effect(() => {
      // Don't use effects to pass data to children
      this.childComponent.updateData(this.parentSignal());
    });
  }
}

// βœ… USE INPUT SIGNALS INSTEAD
export class ChildComponent {
  // Angular 17+ input signals
  data = input<string>('');

  // Or computed based on inputs
  processedData = computed(() =>
    this.data().toUpperCase()
  );
}

Enter fullscreen mode Exit fullscreen mode

Best Practices: Your Signal Effect Playbook

After helping dozens of teams migrate to signals, here's my battle-tested playbook:

🎯 Rule #1: Computed > Effect

If you can express something as a computed signal, always choose that over an effect.

// Ask yourself: "Am I deriving state or causing a side effect?"

// Deriving state? β†’ Use computed()
totalPrice = computed(() => this.items().reduce((sum, item) => sum + item.price, 0));

// Side effect? β†’ Use effect()
effect(() => {
  analytics.track('cart-updated', { itemCount: this.items().length });
});

Enter fullscreen mode Exit fullscreen mode

🎯 Rule #2: Always Handle Cleanup

export class WebSocketService {
  messages = signal<string[]>([]);

  private socketEffect = effect((onCleanup) => {
    const socket = new WebSocket('ws://localhost:8080');

    socket.onmessage = (event) => {
      this.messages.update(msgs => [...msgs, event.data]);
    };

    // THIS IS CRUCIAL!
    onCleanup(() => {
      socket.close();
      console.log('WebSocket connection cleaned up');
    });
  });
}

Enter fullscreen mode Exit fullscreen mode

🎯 Rule #3: Use allowSignalWrites Sparingly

effect(() => {
  const value = this.sourceSignal();

  // Only when absolutely necessary
  untracked(() => {
    this.derivedSignal.set(value * 2);
  });
}, { allowSignalWrites: true });

Enter fullscreen mode Exit fullscreen mode

🎯 Rule #4: Leverage toSignal and toObservable

Bridge the gap between RxJS and signals elegantly:

export class DataService {
  // Observable to Signal
  userData$ = this.http.get('/api/user');
  userSignal = toSignal(this.userData$, { initialValue: null });

  // Signal to Observable
  searchTerm = signal('');
  searchResults$ = toObservable(this.searchTerm).pipe(
    debounceTime(300),
    switchMap(term => this.http.get(`/api/search?q=${term}`))
  );
}

Enter fullscreen mode Exit fullscreen mode

Unit Testing Signals and Effects

Don't forget to test! Here's how to properly test components with signals:

describe('ShoppingCartComponent', () => {
  let component: ShoppingCartComponent;
  let fixture: ComponentFixture<ShoppingCartComponent>;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [ShoppingCartComponent]
    });

    fixture = TestBed.createComponent(ShoppingCartComponent);
    component = fixture.componentInstance;
  });

  it('should update computed signal when base signal changes', () => {
    // Initial state
    expect(component.cartMessage()).toBe('Your cart is empty πŸ›’');

    // Update signal
    component.itemCount.set(3);
    fixture.detectChanges();

    // Computed should auto-update
    expect(component.cartMessage()).toBe('You have 3 items in your cart');
  });

  it('should trigger effect on signal change', fakeAsync(() => {
    const spy = jasmine.createSpy('effectSpy');

    // Create test effect
    const testEffect = effect(() => {
      component.itemCount();
      spy();
    });

    // Change signal
    component.itemCount.set(5);

    // Effects run asynchronously
    tick();

    expect(spy).toHaveBeenCalledTimes(2); // Initial + update

    // Cleanup
    testEffect.destroy();
  }));

  it('should cleanup effects on destroy', () => {
    spyOn(localStorage, 'setItem');

    const service = new UserPreferencesService();
    service.theme.set('dark');

    // Effect should have fired
    expect(localStorage.setItem).toHaveBeenCalledWith('user-theme', 'dark');

    // Cleanup
    service.ngOnDestroy();

    // Further changes shouldn't trigger effect
    service.theme.set('light');
    expect(localStorage.setItem).toHaveBeenCalledTimes(1);
  });
});

Enter fullscreen mode Exit fullscreen mode

πŸ’‘ Bonus Tips: Level Up Your Signal Game

1. Use Signal Inputs for Better Performance

// Instead of @Input() + ngOnChanges
export class UserCardComponent {
  // Signal inputs are automatically reactive!
  userName = input.required<string>();
  userAge = input<number>(0);

  // Computed values update automatically
  userDisplay = computed(() =>
    `${this.userName()} (${this.userAge()} years old)`
  );
}

Enter fullscreen mode Exit fullscreen mode

2. Create Custom Signal Operators

// Create reusable signal patterns
export function createDebouncedSignal<T>(initialValue: T, delay = 300) {
  const source = signal(initialValue);
  const debounced = signal(initialValue);

  effect((onCleanup) => {
    const value = source();
    const timer = setTimeout(() => {
      debounced.set(value);
    }, delay);

    onCleanup(() => clearTimeout(timer));
  });

  return { source, debounced };
}

// Usage
const { source: searchInput, debounced: debouncedSearch } =
  createDebouncedSignal('');

Enter fullscreen mode Exit fullscreen mode

3. Monitor Signal Performance

// Debug performance issues
effect(() => {
  console.time('expensive-effect');

  // Your effect logic here
  const result = this.expensiveComputation();

  console.timeEnd('expensive-effect');

  if (performance.now() > 16) {  // Longer than one frame
    console.warn('Effect taking too long!');
  }
});

Enter fullscreen mode Exit fullscreen mode

Recap: Your Signal Effect Checklist βœ…

Let's wrap this up with a quick checklist you can bookmark:

  1. Computed for derived state, effects for side effects - This is your north star
  2. Always cleanup in effects - Memory leaks are not a feature
  3. Never nest effects - Your performance will thank you
  4. Debounce expensive operations - Not every keystroke needs an API call
  5. Test your reactive code - Signals make testing easier, not optional
  6. Use signal inputs - They're the future of Angular components
  7. Profile your effects - If it's slow, measure it

Remember: Signals are powerful, but with great power comes great responsibility. Use effects wisely, and your Angular apps will be faster, cleaner, and more maintainable than ever.


πŸš€ Let's Keep This Conversation Going!

What did you think? πŸ’¬

Did this clear up your confusion about effects? What's the gnarliest signal bug you've encountered? Drop a comment belowβ€”I read and respond to every single one!

Found this helpful? πŸ‘

If this article saved you from an infinite loop or helped you understand signals better, smash that clap button! Your claps help other devs discover these tips too. (Pro tip: you can clap up to 50 timesβ€”just saying πŸ˜‰)

Want more Angular deep dives? πŸ“¬

I publish one in-depth Angular article every week. Follow me here on Medium or subscribe to my newsletter for exclusive tips, early access to articles, and bonus code snippets that don't make it into the posts.

Your turn to take action:

  1. Refactor one effect in your codebase using what you learned
  2. Share this with a teammate who's struggling with signals
  3. Comment with your #1 takeaway

Let's build better Angular apps together! See you in the next one where we'll tackle *"Angular's New Control Flow: Why ngFor is Dead" πŸ”₯


πŸš€ 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)