Code Quality beginner

Common Signal Pitfalls

Avoid common mistakes and antipatterns when working with Angular Signals.

Alex Muturi
November 7, 2025
16 min read
#signals #pitfalls #antipatterns #common-mistakes

Common Signal Pitfalls

Learn to recognize and avoid the most common mistakes when working with signals.

Pitfall #1: Mutating Signal Values

// ❌ WRONG: Mutation doesn't trigger updates
const items = signal([1, 2, 3]);
items().push(4); // No update!

// ✅ CORRECT: Create new array
items.set([...items(), 4]);
items.update(arr => [...arr, 4]);

Pitfall #2: Signal Calls in Non-Reactive Context

// ❌ WRONG: Signal read outside computed/effect
const mySignal = signal(0);
const value = mySignal(); // Just reads once
setTimeout(() => console.log(value), 1000); // Stale value!

// ✅ CORRECT: Read signal when needed
setTimeout(() => console.log(mySignal()), 1000);

Pitfall #3: Circular Dependencies

// ❌ WRONG: Infinite loop
const a = signal(1);
effect(() => {
  a.set(a() + 1); // Keeps triggering itself!
});

// ✅ CORRECT: Use condition or untracked
effect(() => {
  if (untracked(() => a()) < 10) {
    a.update(n => n + 1);
  }
});

Pitfall #4: Over-Using Effects

// ❌ WRONG: Effect for derived state
const firstName = signal('John');
const lastName = signal('Doe');
const fullName = signal('');

effect(() => {
  fullName.set(`${firstName()} ${lastName()}`);
});

// ✅ CORRECT: Use computed
const fullNameComputed = computed(() => `${firstName()} ${lastName()}`);

Pitfall #5: Not Cleaning Up Effects

// ❌ WRONG: Memory leak
effect(() => {
  setInterval(() => console.log('tick'), 1000);
  // Never cleared!
});

// ✅ CORRECT: Clean up
effect((onCleanup) => {
  const timer = setInterval(() => console.log('tick'), 1000);
  onCleanup(() => clearInterval(timer));
});

Pitfall #6: Unnecessary Object Spreading

// ❌ INEFFICIENT: Spreads entire large object
const largeData = { name: 'test' };
const data = signal({ id: 1, ...largeData });
const id = computed(() => ({ ...data() }).id); // Unnecessary copy

// ✅ EFFICIENT: Extract only what's needed
const idEfficient = computed(() => data().id);

Pitfall #7: Signal in Constructor Before Injection

// ❌ WRONG: Signal before super()
class BaseComponent {}

class MyComponentWrong extends BaseComponent {
  count = signal(0); // Error if BaseComponent needs injection context
  
  constructor() {
    super();
  }
}

// ✅ CORRECT: Initialize after super()
class MyComponentCorrect extends BaseComponent {
  count!: WritableSignal<number>;
  
  constructor() {
    super();
    this.count = signal(0);
  }
}

Pitfall #8: Forgetting Signal Is a Function

// ❌ WRONG: Treating signal as value
const count = signal(0);
// @ts-expect-error
if (count > 5) { } // Comparing function to number!

// ✅ CORRECT: Call the signal
if (count() > 5) { }

Pitfall #9: Async in Computed

// ❌ WRONG: Async computed
const dataWrong = computed(async () => {
  return await fetch('/api/data'); // Doesn't work!
});

// ✅ CORRECT: Use effect with signal
const dataCorrect = signal<any>(null);
effect(async () => {
  const result = await fetch('/api/data');
  dataCorrect.set(result);
});

Pitfall #10: Multiple Signal Calls

// ❌ INEFFICIENT: Multiple signal calls
const user = signal({ name: 'Alice', age: 30 });
const name = user().name;
const age = user().age;

// ✅ EFFICIENT: Single call
const userData = user();
const name2 = userData.name;
const age2 = userData.age;

Quick Checklist

✅ Always use immutable updates
✅ Clean up effects with onCleanup
✅ Use computed for derived state
✅ Call signals with ()
✅ Avoid mutations
✅ Watch for circular dependencies
✅ Keep effects focused on side effects
✅ Test signal-based code

Key Takeaways

  1. Immutability is essential
  2. Effects are for side effects only
  3. Computed for derived state
  4. Always call signals with ()
  5. Clean up resources
  6. Avoid circular dependencies

Next Steps

References

Share this article