Angular Effects: The API You Should Never Reach For First

The Expert's Warning
Last week, Alex Rickabaugh from the Angular team dropped this bombshell during an Angular Nation hangout:
"There are no situations where effect is good, only situations where it is appropriate"
That's the lead architect of Angular Signals telling you to avoid one of the most prominent APIs in the signals system. Here's why.
The Problem Every Developer Hits
You're building a select component. Options come from a parent component as input. You track the selected index internally. When options change, you need to reset the selection.
Most developers write this:
@Component({
selector: 'app-select',
template: `
<li *ngFor="let option of options(); let i = index"
(click)="selectIndex(i)"
[class.selected]="selectedIndex() === i">
{{ option }}
</li>
`
})
export class SelectComponent {
options = input<string[]>([]);
selectedIndex = signal(-1);
constructor() {
// 🚨 RED FLAG: This feels natural but creates problems
effect(() => {
this.options(); // Reading but not using the value
this.selectedIndex.set(-1); // Reset when options change
});
}
selectIndex(index: number) {
this.selectedIndex.set(index);
}
}
This looks reasonable. It works. It's also wrong.
The Hidden Time Bomb
Here's what's happening under the hood:
- Multiple Sources of Truth:
optionsandselectedIndexare independent reactive values - Timing Issues: Effects run asynchronously during change detection
- Glitches: There's a window where options changed but selection hasn't reset yet
- Synchronization Hell: You're manually coordinating reactive state
Alex calls this "glitches in the Matrix" — moments where your app state doesn't make sense.
The Single Source of Truth Solution
Instead of synchronizing two separate signals, create one source of truth:
@Component({
selector: 'app-select',
template: `
<li *ngFor="let option of state().options; let i = index"
(click)="selectIndex(i)"
[class.selected]="state().selectedIndex() === i">
{{ option }}
</li>
`
})
export class SelectComponent {
options = input<string[]>([]);
// ✅ Single source of truth
state = computed(() => ({
options: this.options(),
selectedIndex: signal(-1) // Fresh signal when options change
}));
selectIndex(index: number) {
this.state().selectedIndex.set(index);
}
}
Mind = Blown.
Every time options changes, the computed creates an entirely new selectedIndex signal. No synchronization needed. No glitches. One source of truth.
Real-World Cases Where This Matters
1. Form State with Dynamic Fields
// ❌ BAD: Synchronizing form state with field changes
@Component({...})
export class DynamicForm {
fields = input<FieldConfig[]>([]);
formValues = signal({});
constructor() {
effect(() => {
const currentFields = this.fields();
// Reset form when fields change
this.formValues.set({});
});
}
}
// ✅ GOOD: Single source of truth
@Component({...})
export class DynamicForm {
fields = input<FieldConfig[]>([]);
formState = computed(() => ({
fields: this.fields(),
values: signal({}) // Fresh form state per field set
}));
}
2. Pagination with Filtered Data
// ❌ BAD: Multiple signals to sync
export class DataTable {
data = input<Item[]>([]);
currentPage = signal(1);
filter = signal('');
constructor() {
effect(() => {
this.data();
this.filter();
this.currentPage.set(1); // Reset page on data/filter change
});
}
}
// ✅ GOOD: Derived state
export class DataTable {
data = input<Item[]>([]);
filter = signal('');
tableState = computed(() => {
const filteredData = this.data().filter(item =>
item.name.includes(this.filter())
);
return {
data: filteredData,
currentPage: signal(1) // Fresh page state per filter
};
});
}
When Effects ARE the Right Tool
Effects should only bridge the reactive world (signals) with the non-reactive world:
@Component({...})
export class AnalyticsComponent {
userId = input<string>();
constructor() {
// ✅ GOOD: Syncing with external APIs
effect(() => {
const user = this.userId();
analytics.track('user-viewed', { userId: user });
});
// ✅ GOOD: DOM manipulation not possible via templates
effect(() => {
const theme = this.theme();
document.documentElement.className = `theme-${theme}`;
});
// ✅ GOOD: LocalStorage sync
effect(() => {
const preferences = this.userPreferences();
localStorage.setItem('prefs', JSON.stringify(preferences));
});
}
}
The Mental Model Shift
Stop thinking: "I have X and Y that need to stay in sync"
Start thinking: "What's the true source of truth that everything else derives from?"
Most "synchronization" problems are actually state modeling problems. Fix the model, and synchronization disappears.
The Broader Pattern
This isn't unique to Angular. MobX documentation starts with "You probably don't need reactions."
The pattern: Powerful, flexible tools with hidden complexity should be your last resort, not your first reach.
Takeaways for Your Codebase
- Audit your effects: Are you setting signals inside them? Red flag.
- Look for multiple sources of truth: Can you combine them into one computed?
- Use
computed()andlinkedSignal()for derived state - Reserve effects for external API integration only
- When in doubt, step away from the keyboard and model the problem on paper first
The Angular team built effects because sometimes you need them. But they also put every guardrail they could think of to discourage overuse.
Listen to the experts: effects should be your last choice, not your first instinct.
What's the most complex effect you've written that could probably be a computed instead? Hit reply and tell me about it.
Sources: