Are you sabotaging your Angular app’s performance without even knowing it?
Angular Signals have revolutionized how we handle reactive state in our applications, but with great power comes great responsibility. As more developers adopt this powerful feature, I’ve noticed some patterns that could spell trouble for your app’s performance and maintainability.
Here’s the thing: Even experienced Angular developers are making these mistakes. I’ve seen production apps crash, performance tank, and developers scratch their heads wondering why their “modern” code isn’t working as expected.
In this article, you’ll discover the 5 most common (and dangerous) mistakes developers make with Angular Signals, plus 3 bonus pitfalls that could save you hours of debugging. By the end, you’ll have actionable strategies to write cleaner, more performant Angular code.
💡 Quick win: Hit that 👏 if you’ve ever been confused about when to use Signals vs Observables – you’re not alone!
Mistake #1: Not Using Signals for Dynamic State (The Future-Proofing Killer)
The Problem: You’re still using traditional properties for state that changes during your app’s lifecycle.
Here’s what I see way too often:
@Component({
template: `
{{ userStatus }}
`
})
export class UserComponent {
userStatus = 'inactive'; // ❌ This will bite you later
updateStatus(newStatus: string) {
this.userStatus = newStatus; // ❌ No change detection optimization
}
}
Why this hurts: When Angular rolls out Signal Components (and they will), this code will break. Your UI won’t update because Angular won’t know the state changed.
The Fix:
@Component({
template: `
{{ userStatus() }}
`
})
export class UserComponent {
userStatus = signal('inactive'); // ✅ Future-proof
updateStatus(newStatus: string) {
this.userStatus.set(newStatus); // ✅ Angular knows what changed
}
}
Pro tip: Even if you’re updating from a subscription, you won’t need markForCheck()
anymore! Angular’s change detection becomes laser-focused on what actually changed.
👇 Have you been caught off-guard by change detection issues? Drop a comment below!
Mistake #2: Signal Overuse (When More Isn’t Better)
The Problem: Using signals for everything, even when they’re not rendered or don’t need reactivity.
I’ve seen developers go signal-crazy:
@Component({})
export class OverEnthusiasticComponent {
private isFirstClick = signal(true); // ❌ Overkill
private debugMode = signal(false); // ❌ Unnecessary overhead
handleClick() {
if (this.isFirstClick()) {
this.isFirstClick.set(false);
return;
}
// Handle subsequent clicks
}
}
Why this matters: You’re adding unnecessary reactive overhead for values that never need to trigger UI updates.
The Fix:
@Component({})
export class SmartComponent {
#isFirstClick = true; // ✅ Simple and efficient
#debugMode = false; // ✅ Private and lightweight
handleClick() {
if (this.#isFirstClick) {
this.#isFirstClick = false;
return;
}
// Handle subsequent clicks
}
}
Rule of thumb: If it’s not rendered in the template and doesn’t need reactivity, keep it simple.
📬 Want more performance tips? Subscribe to my newsletter for weekly Angular insights!
Mistake #3: Unnecessary Signal Conversions (The Verbose Trap)
The Problem: Converting Observables to Signals just to use them in effects, when you could work with the Observable directly.
Here’s the verbose approach I see too often:
@Component({})
export class VerboseComponent {
form = new FormGroup({
email: new FormControl(''),
password: new FormControl('')
});
constructor() {
// ❌ Unnecessary conversion
const formValue = toSignal(this.form.valueChanges);
effect(() => {
const value = formValue();
// Handle form changes
console.log('Form changed:', value);
});
}
}
The cleaner approach:
@Component({})
export class CleanComponent {
form = new FormGroup({
email: new FormControl(''),
password: new FormControl('')
});
constructor() {
// ✅ Direct and predictable
this.form.valueChanges.pipe(
takeUntilDestroyed()
).subscribe(value => {
// Handle form changes
console.log('Form changed:', value);
});
}
}
Key insight: Effects run during change detection, while Observables run immediately. Choose based on your timing needs.
Mistake #4: Unanalyzed Effect Dependencies (The Hidden Subscription Bomb)
The Problem: Writing effects without considering what signals they’re actually tracking.
This innocent-looking code can cause performance nightmares:
@Component({})
export class ProblematicComponent {
#userStore = inject(UserStore);
constructor() {
effect(() => {
const currentAccount = this.#userStore.currentAccount();
const defaultAccount = this.#userStore.defaultAccount();
const activeAccount = currentAccount || defaultAccount;
if (!activeAccount) return;
// ❌ This might run twice for the same account!
this.fetchAccountData(activeAccount);
});
}
}
What happens: If both currentAccount
and defaultAccount
change to the same value, your effect runs twice, making duplicate API calls.
The optimized solution:
@Component({})
export class OptimizedComponent {
#userStore = inject(UserStore);
constructor() {
// ✅ Single source of truth
const activeAccount = computed(() =>
this.#userStore.currentAccount() || this.#userStore.defaultAccount()
);
effect(() => {
const account = activeAccount();
if (!account) return;
// ✅ Runs only when the actual active account changes
this.fetchAccountData(account);
});
}
}
Pro technique: Use explicitEffect
from ngxtension for maximum control:
import { explicitEffect } from 'ngxtension/explicit-effect';
constructor() {
const activeAccount = computed(() =>
this.#userStore.currentAccount() || this.#userStore.defaultAccount()
);
// ✅ Crystal clear dependencies
explicitEffect([activeAccount], ([account]) => {
if (!account) return;
this.fetchAccountData(account);
});
}
💬 Have you been bitten by unexpected effect runs? Share your story in the comments!
Mistake #5: Mixing Async Pipes with Signals (The Inconsistency Problem)
The Problem: Using async pipes in a Signal-heavy codebase creates inconsistency and missed opportunities.
Mixed approach:
@Component({
template: `
@for (item of items$ | async; track item.id) {
{{ item.name }}
}
@for (item of items(); track item.id) {
{{ item.name }}
}
`
})
export class InconsistentComponent {
#service = inject(DataService);
// ❌ Requires async pipe, harder to access in TS
items$ = this.#service.getData();
// ✅ Easy synchronous access, no async pipe needed
items = toSignal(this.#service.getData(), { initialValue: [] });
}
Benefits of the Signal approach:
- Smaller bundle size (no async pipe)
- Synchronous access to current state
- Consistent reactive patterns
- Better TypeScript integration
Bonus Mistake #6: Ignoring Signal Equality (The Unnecessary Render Trap)
The Problem: Not understanding how Signal equality works, leading to unnecessary re-renders.
@Component({})
export class InefficientComponent {
users = signal<User[]>([]);
addUser(user: User) {
// ❌ Always creates new array reference
this.users.set([...this.users(), user]);
}
updateUser(id: string, updates: Partial<User>) {
// ❌ Creates new array even if user wasn't found
this.users.set(
this.users().map(u => u.id === id ? { ...u, ...updates } : u)
);
}
}
The optimized approach:
@Component({})
export class EfficientComponent {
users = signal<User[]>([]);
addUser(user: User) {
// ✅ Only update if user is actually new
if (this.users().find(u => u.id === user.id)) return;
this.users.update(users => [...users, user]);
}
updateUser(id: string, updates: Partial<User>) {
// ✅ Only update if user exists and changes
this.users.update(users => {
const index = users.findIndex(u => u.id === id);
if (index === -1) return users; // No change
const updatedUser = { ...users[index], ...updates };
if (JSON.stringify(updatedUser) === JSON.stringify(users[index])) {
return users; // No actual change
}
const newUsers = [...users];
newUsers[index] = updatedUser;
return newUsers;
});
}
}
Bonus Mistake #7: Not Using Computed Signals for Derived State
The Problem: Manually managing derived state instead of letting Angular handle it.
@Component({})
export class ManualComponent {
firstName = signal('');
lastName = signal('');
fullName = signal(''); // ❌ Manual management
updateFirstName(name: string) {
this.firstName.set(name);
this.fullName.set(`${name} ${this.lastName()}`); // ❌ Manual sync
}
updateLastName(name: string) {
this.lastName.set(name);
this.fullName.set(`${this.firstName()} ${name}`); // ❌ Manual sync
}
}
The reactive approach:
@Component({})
export class ReactiveComponent {
firstName = signal('');
lastName = signal('');
// ✅ Automatically updates when dependencies change
fullName = computed(() => `${this.firstName()} ${this.lastName()}`);
updateFirstName(name: string) {
this.firstName.set(name);
// ✅ fullName updates automatically
}
updateLastName(name: string) {
this.lastName.set(name);
// ✅ fullName updates automatically
}
}
👏 If this helped you avoid a potential bug, give it a clap!
The Bottom Line
Angular Signals are powerful, but they require a shift in thinking. The key is knowing when to use them, how to use them efficiently, and what to avoid.
Quick recap of the main pitfalls:
- Not using Signals for dynamic state – Future-proof your code
- Overusing Signals – Keep non-reactive state simple
- Unnecessary conversions – Don’t convert just to convert
- Unanalyzed effect dependencies – Think about what you’re tracking
- Mixing async pipes with Signals – Stay consistent
- Ignoring Signal equality – Optimize your updates
- Manual derived state – Let computed signals do the work
What’s Next?
What did you think? Which mistake surprised you the most? Have you encountered any of these in your own projects? Drop a comment below and let’s discuss! 💬
Found this helpful? Hit that 👏 button – it helps other developers discover these tips and avoid the same pitfalls.
Want more Angular insights like this? Follow me for weekly deep-dives into Angular best practices, performance tips, and the latest framework updates. I also send out a newsletter with exclusive content and early access to new articles. 📬
Remember: The best code is not just working code – it’s code that works efficiently and stands the test of time. Angular Signals can help you write that code, but only if you use them wisely.
🚀 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! 🧪🧠🚀