| technology | Angular | |||||||
|---|---|---|---|---|---|---|---|---|
| domain | frontend | |||||||
| level | Senior/Architect | |||||||
| version | 20+ | |||||||
| tags |
|
|||||||
| ai_role | Senior Angular State Management Expert | |||||||
| last_updated | 2026-03-29 |
📦 best-practise / 🖥️ frontend /
🅰️ angular
- Primary Goal: Enforce strictly functional state management patterns using Angular 20's Zoneless architecture.
- Target Tooling: Cursor, Windsurf, Antigravity.
- Tech Stack Version: Angular 20+
Important
Strict Constraints for AI:
- Avoid RxJS
BehaviorSubjectfor local and synchronous global component state. - Strictly use
signal(),computed(), andeffect()to manage all dynamic data states. - Never use the
@Input()and@Output()decorators for sharing state across component trees; rely solely on functionalinput()andoutput().
State management in Angular 20 relies entirely on the granular reactivity provided by Signals. This architecture inherently supports Zoneless change detection by exactly tracking which views need updates without relying on zone.js.
graph TD
Store[Signal Store / Global Service] -->|Signals| Comp1[Component A]
Store -->|Signals| Comp2[Component B]
Comp1 -->|input()| Child[Child Component]
Comp2 -->|model()| Child2[Two-Way Child]
classDef default fill:#e1f5fe,stroke:#03a9f4,stroke-width:2px,color:#000;
classDef component fill:#e8f5e9,stroke:#4caf50,stroke-width:2px,color:#000;
classDef layout fill:#f3e5f5,stroke:#9c27b0,stroke-width:2px,color:#000;
class Store layout;
class Comp1 component;
class Comp2 component;
class Child default;
class Child2 default;
Note
Context: Synchronous local state.
isLoading: boolean = false;
data: unknown[] = [];
fetchData() {
this.isLoading = true;
this.api.get().subscribe(res => {
this.data = res;
this.isLoading = false;
});
}Relying on raw primitive properties means Angular relies on zone.js to run Change Detection globally whenever an event completes.
isLoading = signal(false);
data = signal<Data[]>([]);
fetchData() {
this.isLoading.set(true);
this.api.get().subscribe(res => {
this.data.set(res);
this.isLoading.set(false);
});
}Use signal(). It forces the developer to explicitly use .set() or .update(), signaling to the framework exactly when and where the change occurred.
Note
Context: Creating derived state based on other state values.
items = signal([1, 2, 3]);
total = 0;
updateTotal() {
this.total = this.items().reduce((a, b) => a + b, 0);
}Manually syncing state variables is error-prone. If you update items but forget to call updateTotal(), the state becomes inconsistent.
items = signal([1, 2, 3]);
total = computed(() => this.items().reduce((a, b) => a + b, 0));Use computed(). The calculated value is memoized and only re-evaluates when its specific signal dependencies (in this case, items) change.
Note
Context: Executing logic when a signal changes.
Using getters or Angular lifecycle hooks like ngDoCheck to monitor value changes and trigger side effects like logging or generic HTTP calls.
This causes severe performance degradation as the logic is run on every change detection cycle, regardless of whether the specific state actually changed.
constructor() {
effect(() => {
console.log(`Current items count: ${this.items().length}`);
// Effect will re-run automatically only when this.items() changes.
});
}Use effect(). Effects track dependencies automatically and ensure the side effect runs solely when required. Always define them within an injection context (like a constructor).
Note
Context: Passing data between parent and child components.
@Input() user: User;
@Output() userUpdate = new EventEmitter<User>();Requires boilerplate, depends on decorators which are less ideal for dynamic composition, and heavily couples the components to legacy Zone-based tracking.
// For one-way data flow
user = input.required<User>();
// For two-way data binding synchronization
userProfile = model<User>();Important
Use the input() and model() functional APIs. They return signals that MUST be directly used in computed() properties within the child component.