Standalone Components (REQUIRED)
Components are standalone by default. Do NOT set standalone: true.
@Component({
selector: 'app-user',
imports: [CommonModule],
changeDetection: ChangeDetectionStrategy.OnPush,
template: ...
})
export class UserComponent {}
Input/Output Functions (REQUIRED)
// ✅ ALWAYS: Function-based
readonly user = input.required
// ❌ NEVER: Decorators
@Input() user: User;
@Output() selected = new EventEmitter
Signals for State (REQUIRED) readonly count = signal(0); readonly doubled = computed(() => this.count() * 2);
// Update this.count.set(5); this.count.update(prev => prev + 1);
// Side effects effect(() => localStorage.setItem('count', this.count().toString()));
NO Lifecycle Hooks (REQUIRED)
Signals replace lifecycle hooks. Do NOT use ngOnInit, ngOnChanges, ngOnDestroy.
// ❌ NEVER: Lifecycle hooks ngOnInit() { this.loadUser(); }
ngOnChanges(changes: SimpleChanges) { if (changes['userId']) { this.loadUser(); } }
// ✅ ALWAYS: Signals + effect
readonly userId = input.required
private userEffect = effect(() => { // Runs automatically when userId() changes this.loadUser(this.userId()); });
// ✅ For derived data, use computed readonly displayName = computed(() => this.user()?.name ?? 'Guest');
When to Use What Need Use React to input changes effect() watching the input signal Derived/computed state computed() Side effects (API calls, localStorage) effect() Cleanup on destroy DestroyRef + inject() // Cleanup example private readonly destroyRef = inject(DestroyRef);
constructor() { const subscription = someObservable$.subscribe(); this.destroyRef.onDestroy(() => subscription.unsubscribe()); }
inject() Over Constructor (REQUIRED) // ✅ ALWAYS private readonly http = inject(HttpClient);
// ❌ NEVER constructor(private http: HttpClient) {}
Native Control Flow (REQUIRED)
@if (loading()) {
No items
} }@switch (status()) { @case ('active') { Active } @default { Unknown } }
RxJS - Only When Needed
Signals are the default. Use RxJS ONLY for complex async operations.
Use Signals Use RxJS
Component state Combining multiple streams
Derived values Debounce/throttle
Simple async (single API call) Race conditions
Input/Output WebSockets, real-time
Complex error retry logic
// ✅ Simple API call - use signals
readonly user = signal
async loadUser(id: string) {
this.loading.set(true);
this.user.set(await firstValueFrom(this.http.get/api/users/${id})));
this.loading.set(false);
}
// ✅ Complex stream - use RxJS
readonly searchResults$ = this.searchTerm$.pipe(
debounceTime(300),
distinctUntilChanged(),
switchMap(term => this.http.get/api/search?q=${term}))
);
// Convert to signal when needed in template readonly searchResults = toSignal(this.searchResults$, { initialValue: [] });
Zoneless Angular (REQUIRED)
Angular is zoneless. Use provideZonelessChangeDetection().
bootstrapApplication(AppComponent, { providers: [provideZonelessChangeDetection()] });
Remove ZoneJS:
npm uninstall zone.js
Remove from angular.json polyfills: zone.js and zone.js/testing.
Zoneless Requirements Use OnPush change detection Use signals for state (auto-notifies Angular) Use AsyncPipe for observables Use markForCheck() when needed Resources https://angular.dev/guide/signals https://angular.dev/guide/templates/control-flow https://angular.dev/guide/zoneless