Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Modern Angular (v20+) expert with deep knowledge of Signals, Standalone Components, Zoneless applications, SSR/Hydration, and reactive patterns.
| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-01 | ✗→✓ | ▲ Improved | 277% | 0% |
| case-11 | ✗→✓ | ▲ Improved | 385% | 0% |
| case-15 | ✗→✓ | ▲ Improved | 231% | 0% |
| case-02 | ✓→✗ | ▼ Worse | 203% | 0% |
| case-03 | ✓→✓ | = Same ✓ | 302% | 0% |
Master modern Angular development with Signals, Standalone Components, Zoneless applications, SSR/Hydration, and the latest reactive patterns.
angular-migration skilltypescript-expert skill| Version | Release | Key Features | | -------------- | ------- | ------------------------------------------------------ | | Angular 20 | Q2 2025 | Signals stable, Zoneless stable, Incremental hydration | | Angular 21 | Q4 2025 | Signals-first default, Enhanced SSR | | Angular 22 | Q2 2026 | Signal Forms, Selectorless components |
Signals are Angular's fine-grained reactivity system, replacing zone.js-based change detection.
typescriptimport { signal, computed, effect } from "@angular/core"; // Writable signal const count = signal(0); // Read value console.log(count()); // 0 // Update value count.set(5); // Direct set count.update((v) => v + 1); // Functional update // Computed (derived) signal const doubled = computed(() => count() * 2); // Effect (side effects) effect(() => { console.log(`Count changed to: ${count()}`); });
typescriptimport { Component, input, output, model } from "@angular/core"; @Component({ selector: "app-user-card", standalone: true, template: ` <div class="card"> <h3>{{ name() }}</h3> <span>{{ role() }}</span> <button (click)="select.emit(id())">Select</button> </div> `, }) export class UserCardComponent { // Signal inputs (read-only) id = input.required<string>(); name = input.required<string>(); role = input<string>("User"); // With default // Output select = output<string>(); // Two-way binding (model) isSelected = model(false); } // Usage: // <app-user-card [id]="'123'" [name]="'John'" [(isSelected)]="selected" />
typescriptimport { Component, viewChild, viewChildren, contentChild, } from "@angular/core"; @Component({ selector: "app-container", standalone: true, template: ` <input #searchInput /> <app-item *ngFor="let item of items()" /> `, }) export class ContainerComponent { // Signal-based queries searchInput = viewChild<ElementRef>("searchInput"); items = viewChildren(ItemComponent); projectedContent = contentChild(HeaderDirective); focusSearch() { this.searchInput()?.nativeElement.focus(); } }
| Use Case | Signals | RxJS | | ----------------------- | --------------- | -------------------------------- | | Local component state | ✅ Preferred | Overkill | | Derived/computed values | ✅ computed() | combineLatest works | | Side effects | ✅ effect() | tap operator | | HTTP requests | ❌ | ✅ HttpClient returns Observable | | Event streams | ❌ | ✅ fromEvent, operators | | Complex async flows | ❌ | ✅ switchMap, mergeMap |
Standalone components are self-contained and don't require NgModule declarations.
typescriptimport { Component } from "@angular/core"; import { CommonModule } from "@angular/common"; import { RouterLink } from "@angular/router"; @Component({ selector: "app-header", standalone: true, imports: [CommonModule, RouterLink], // Direct imports template: ` <header> <a routerLink="/">Home</a> <a routerLink="/about">About</a> </header> `, }) export class HeaderComponent {}
typescript// main.ts import { bootstrapApplication } from "@angular/platform-browser"; import { provideRouter } from "@angular/router"; import { provideHttpClient } from "@angular/common/http"; import { AppComponent } from "./app/app.component"; import { routes } from "./app/app.routes"; bootstrapApplication(AppComponent, { providers: [provideRouter(routes), provideHttpClient()], });
typescript// app.routes.ts import { Routes } from "@angular/router"; export const routes: Routes = [ { path: "dashboard", loadComponent: () => import("./dashboard/dashboard.component").then( (m) => m.DashboardComponent, ), }, { path: "admin", loadChildren: () => import("./admin/admin.routes").then((m) => m.ADMIN_ROUTES), }, ];
Zoneless applications don't use zone.js, improving performance and debugging.
typescript// main.ts import { bootstrapApplication } from "@angular/platform-browser"; import { provideZonelessChangeDetection } from "@angular/core"; import { AppComponent } from "./app/app.component"; bootstrapApplication(AppComponent, { providers: [provideZonelessChangeDetection()], });
typescriptimport { Component, signal, ChangeDetectionStrategy } from "@angular/core"; @Component({ selector: "app-counter", standalone: true, changeDetection: ChangeDetectionStrategy.OnPush, template: ` <div>Count: {{ count() }}</div> <button (click)="increment()">+</button> `, }) export class CounterComponent { count = signal(0); increment() { this.count.update((v) => v + 1); // No zone.js needed - Signal triggers change detection } }
bashng add @angular/ssr
typescript// app.config.ts import { ApplicationConfig } from "@angular/core"; import { provideClientHydration, withEventReplay, } from "@angular/platform-browser"; export const appConfig: ApplicationConfig = { providers: [provideClientHydration(withEventReplay())], };
typescriptimport { Component } from "@angular/core"; @Component({ selector: "app-page", standalone: true, template: ` <app-hero /> @defer (hydrate on viewport) { <app-comments /> } @defer (hydrate on interaction) { <app-chat-widget /> } `, }) export class PageComponent {}
| Trigger | When to Use | | ---------------- | --------------------------------------- | | on idle | Low-priority, hydrate when browser idle | | on viewport | Hydrate when element enters viewport | | on interaction | Hydrate on first user interaction | | on hover | Hydrate when user hovers | | on timer(ms) | Hydrate after specified delay |
typescript// auth.guard.ts import { inject } from "@angular/core"; import { Router, CanActivateFn } from "@angular/router"; import { AuthService } from "./auth.service"; export const authGuard: CanActivateFn = (route, state) => { const auth = inject(AuthService); const router = inject(Router); if (auth.isAuthenticated()) { return true; } return router.createUrlTree(["/login"], { queryParams: { returnUrl: state.url }, }); }; // Usage in routes export const routes: Routes = [ { path: "dashboard", loadComponent: () => import("./dashboard.component"), canActivate: [authGuard], }, ];
typescriptimport { inject } from '@angular/core'; import { ResolveFn } from '@angular/router'; import { UserService } from './user.service'; import { User } from './user.model'; export const userResolver: ResolveFn<User> = (route) => { const userService = inject(UserService); return userService.getUser(route.paramMap.get('id')!); }; // In routes { path: 'user/:id', loadComponent: () => import('./user.component'), resolve: { user: userResolver } } // In component export class UserComponent { private route = inject(ActivatedRoute); user = toSignal(this.route.data.pipe(map(d => d['user']))); }
typescriptimport { Component, inject } from '@angular/core'; import { HttpClient } from '@angular/common/http'; import { UserService } from './user.service'; @Component({...}) export class UserComponent { // Modern inject() - no constructor needed private http = inject(HttpClient); private userService = inject(UserService); // Works in any injection context users = toSignal(this.userService.getUsers()); }
typescriptimport { InjectionToken, inject } from "@angular/core"; // Define token export const API_BASE_URL = new InjectionToken<string>("API_BASE_URL"); // Provide in config bootstrapApplication(AppComponent, { providers: [{ provide: API_BASE_URL, useValue: "https://api.example.com" }], }); // Inject in service @Injectable({ providedIn: "root" }) export class ApiService { private baseUrl = inject(API_BASE_URL); get(endpoint: string) { return this.http.get(`${this.baseUrl}/${endpoint}`); } }
typescript@Component({ selector: 'app-card', template: ` <div class="card"> <div class="header"> <!-- Select by attribute --> <ng-content select="[card-header]"></ng-content> </div> <div class="body"> <!-- Default slot --> <ng-content></ng-content> </div> </div> ` }) export class CardComponent {} // Usage <app-card> <h3 card-header>Title</h3> <p>Body content</p> </app-card>
typescript// Reusable behaviors without inheritance @Directive({ standalone: true, selector: '[appTooltip]', inputs: ['tooltip'] // Signal input alias }) export class TooltipDirective { ... } @Component({ selector: 'app-button', standalone: true, hostDirectives: [ { directive: TooltipDirective, inputs: ['tooltip: title'] // Map input } ], template: `<ng-content />` }) export class ButtonComponent {}
typescriptimport { Injectable, signal, computed } from "@angular/core"; interface AppState { user: User | null; theme: "light" | "dark"; notifications: Notification[]; } @Injectable({ providedIn: "root" }) export class StateService { // Private writable signals private _user = signal<User | null>(null); private _theme = signal<"light" | "dark">("light"); private _notifications = signal<Notification[]>([]); // Public read-only computed readonly user = computed(() => this._user()); readonly theme = computed(() => this._theme()); readonly notifications = computed(() => this._notifications()); readonly unreadCount = computed( () => this._notifications().filter((n) => !n.read).length, ); // Actions setUser(user: User | null) { this._user.set(user); } toggleTheme() { this._theme.update((t) => (t === "light" ? "dark" : "light")); } addNotification(notification: Notification) { this._notifications.update((n) => [...n, notification]); } }
typescriptimport { Injectable, signal, computed, inject } from "@angular/core"; import { HttpClient } from "@angular/common/http"; import { toSignal } from "@angular/core/rxjs-interop"; @Injectable() export class ProductStore { private http = inject(HttpClient); // State private _products = signal<Product[]>([]); private _loading = signal(false); private _filter = signal(""); // Selectors readonly products = computed(() => this._products()); readonly loading = computed(() => this._loading()); readonly filteredProducts = computed(() => { const filter = this._filter().toLowerCase(); return this._products().filter((p) => p.name.toLowerCase().includes(filter), ); }); // Actions loadProducts() { this._loading.set(true); this.http.get<Product[]>("/api/products").subscribe({ next: (products) => { this._products.set(products); this._loading.set(false); }, error: () => this._loading.set(false), }); } setFilter(filter: string) { this._filter.set(filter); } }
typescriptimport { Component, inject } from "@angular/core"; import { FormBuilder, Validators, ReactiveFormsModule } from "@angular/forms"; @Component({ selector: "app-user-form", standalone: true, imports: [ReactiveFormsModule], template: ` <form [formGroup]="form" (ngSubmit)="onSubmit()"> <input formControlName="name" placeholder="Name" /> <input formControlName="email" type="email" placeholder="Email" /> <button [disabled]="form.invalid">Submit</button> </form> `, }) export class UserFormComponent { private fb = inject(FormBuilder); form = this.fb.group({ name: ["", Validators.required], email: ["", [Validators.required, Validators.email]], }); onSubmit() { if (this.form.valid) { console.log(this.form.value); } } }
typescript// Future Signal Forms API (experimental) import { Component, signal } from '@angular/core'; @Component({...}) export class SignalFormComponent { name = signal(''); email = signal(''); // Computed validation isValid = computed(() => this.name().length > 0 && this.email().includes('@') ); submit() { if (this.isValid()) { console.log({ name: this.name(), email: this.email() }); } } }
typescript@Component({ changeDetection: ChangeDetectionStrategy.OnPush, // Only checks when: // 1. Input signal/reference changes // 2. Event handler runs // 3. Async pipe emits // 4. Signal value changes })
typescript@Component({ template: ` <!-- Immediate loading --> <app-header /> <!-- Lazy load when visible --> @defer (on viewport) { <app-heavy-chart /> } @placeholder { <div class="skeleton" /> } @loading (minimum 200ms) { <app-spinner /> } @error { <p>Failed to load chart</p> } ` })
typescriptimport { NgOptimizedImage } from '@angular/common'; @Component({ imports: [NgOptimizedImage], template: ` <img ngSrc="hero.jpg" width="800" height="600" priority /> <img ngSrc="thumbnail.jpg" width="200" height="150" loading="lazy" placeholder="blur" /> ` })
typescriptimport { ComponentFixture, TestBed } from "@angular/core/testing"; import { CounterComponent } from "./counter.component"; describe("CounterComponent", () => { let component: CounterComponent; let fixture: ComponentFixture<CounterComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [CounterComponent], // Standalone import }).compileComponents(); fixture = TestBed.createComponent(CounterComponent); component = fixture.componentInstance; fixture.detectChanges(); }); it("should increment count", () => { expect(component.count()).toBe(0); component.increment(); expect(component.count()).toBe(1); }); it("should update DOM on signal change", () => { component.count.set(5); fixture.detectChanges(); const el = fixture.nativeElement.querySelector(".count"); expect(el.textContent).toContain("5"); }); });
typescriptimport { ComponentFixture, TestBed } from "@angular/core/testing"; import { ComponentRef } from "@angular/core"; import { UserCardComponent } from "./user-card.component"; describe("UserCardComponent", () => { let fixture: ComponentFixture<UserCardComponent>; let componentRef: ComponentRef<UserCardComponent>; beforeEach(async () => { await TestBed.configureTestingModule({ imports: [UserCardComponent], }).compileComponents(); fixture = TestBed.createComponent(UserCardComponent); componentRef = fixture.componentRef; // Set signal inputs via setInput componentRef.setInput("id", "123"); componentRef.setInput("name", "John Doe"); fixture.detectChanges(); }); it("should display user name", () => { const el = fixture.nativeElement.querySelector("h3"); expect(el.textContent).toContain("John Doe"); }); });
| Pattern | ✅ Do | ❌ Don't | | -------------------- | ------------------------------ | ------------------------------- | | State | Use Signals for local state | Overuse RxJS for simple state | | Components | Standalone with direct imports | Bloated SharedModules | | Change Detection | OnPush + Signals | Default CD everywhere | | Lazy Loading | @defer and loadComponent | Eager load everything | | DI | inject() function | Constructor injection (verbose) | | Inputs | input() signal function | @Input() decorator (legacy) | | Zoneless | Enable for new projects | Force on legacy without testing |
| Issue | Solution | | ------------------------------ | --------------------------------------------------- | | Signal not updating UI | Ensure OnPush + call signal as function count() | | Hydration mismatch | Check server/client content consistency | | Circular dependency | Use inject() with forwardRef | | Zoneless not detecting changes | Trigger via signal updates, not mutations | | SSR fetch fails | Use TransferState or withFetch() |
Other measured skills in the registry, with their headline benchmark lift.