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
- Immutability is essential
- Effects are for side effects only
- Computed for derived state
- Always call signals with ()
- Clean up resources
- Avoid circular dependencies
Next Steps
- Review Immutability Guidelines
- Learn Effect Best Practices
- Master Debugging Signals