angular

Modern Angular (v20+) expert with deep knowledge of Signals, Standalone Components, Zoneless applications, SSR/Hydration, and reactive patterns. Use PROACTIVELY for Angular development, component architecture, state management, performance optimization, and migration to modern patterns.

View Source
name:angulardescription:>-risk:safesource:self

Angular Expert

Master modern Angular development with Signals, Standalone Components, Zoneless applications, SSR/Hydration, and the latest reactive patterns.

When to Use This Skill

  • Building new Angular applications (v20+)

  • Implementing Signals-based reactive patterns

  • Creating Standalone Components and migrating from NgModules

  • Configuring Zoneless Angular applications

  • Implementing SSR, prerendering, and hydration

  • Optimizing Angular performance

  • Adopting modern Angular patterns and best practices
  • Do Not Use This Skill When

  • Migrating from AngularJS (1.x) → use angular-migration skill

  • Working with legacy Angular apps that cannot upgrade

  • General TypeScript issues → use typescript-expert skill
  • Instructions

  • Assess the Angular version and project structure

  • Apply modern patterns (Signals, Standalone, Zoneless)

  • Implement with proper typing and reactivity

  • Validate with build and tests
  • Safety

  • Always test changes in development before production

  • Gradual migration for existing apps (don't big-bang refactor)

  • Keep backward compatibility during transitions

  • Angular Version Timeline

    VersionReleaseKey Features
    Angular 20Q2 2025Signals stable, Zoneless stable, Incremental hydration
    Angular 21Q4 2025Signals-first default, Enhanced SSR
    Angular 22Q2 2026Signal Forms, Selectorless components


    1. Signals: The New Reactive Primitive

    Signals are Angular's fine-grained reactivity system, replacing zone.js-based change detection.

    Core Concepts

    import { 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()});
    });

    Signal-Based Inputs and Outputs

    import { 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" />

    Signal Queries (ViewChild/ContentChild)

    import {
    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();
    }
    }

    When to Use Signals vs RxJS

    Use CaseSignalsRxJS
    Local component state✅ PreferredOverkill
    Derived/computed valuescomputed()combineLatest works
    Side effectseffect()tap operator
    HTTP requests✅ HttpClient returns Observable
    Event streamsfromEvent, operators
    Complex async flowsswitchMap, mergeMap


    2. Standalone Components

    Standalone components are self-contained and don't require NgModule declarations.

    Creating Standalone Components

    import { 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 {}

    Bootstrapping Without NgModule

    // 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()],
    });

    Lazy Loading Standalone Components

    // 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),
    },
    ];


    3. Zoneless Angular

    Zoneless applications don't use zone.js, improving performance and debugging.

    Enabling Zoneless Mode

    // main.ts
    import { bootstrapApplication } from "@angular/platform-browser";
    import { provideZonelessChangeDetection } from "@angular/core";
    import { AppComponent } from "./app/app.component";

    bootstrapApplication(AppComponent, {
    providers: [provideZonelessChangeDetection()],
    });

    Zoneless Component Patterns

    import { 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
    }
    }

    Key Zoneless Benefits

  • Performance: No zone.js patches on async APIs

  • Debugging: Clean stack traces without zone wrappers

  • Bundle size: Smaller without zone.js (~15KB savings)

  • Interoperability: Better with Web Components and micro-frontends

  • 4. Server-Side Rendering & Hydration

    SSR Setup with Angular CLI

    ng add @angular/ssr

    Hydration Configuration

    // app.config.ts
    import { ApplicationConfig } from "@angular/core";
    import {
    provideClientHydration,
    withEventReplay,
    } from "@angular/platform-browser";

    export const appConfig: ApplicationConfig = {
    providers: [provideClientHydration(withEventReplay())],
    };

    Incremental Hydration (v20+)

    import { 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 {}

    Hydration Triggers

    TriggerWhen to Use
    on idleLow-priority, hydrate when browser idle
    on viewportHydrate when element enters viewport
    on interactionHydrate on first user interaction
    on hoverHydrate when user hovers
    on timer(ms)Hydrate after specified delay


    5. Modern Routing Patterns

    Functional Route Guards

    // 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],
    },
    ];

    Route-Level Data Resolvers

    import { 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'])));
    }


    6. Dependency Injection Patterns

    Modern inject() Function

    import { 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());
    }

    Injection Tokens for Configuration

    import { 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});
    }
    }


    7. Component Composition & Reusability

    Content Projection (Slots)

    @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>

    Host Directives (Composition)

    // 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 {}


    8. State Management Patterns

    Signal-Based State Service

    import { Injectable, signal, computed } from "@angular/core";

    interface AppState {
    <div class="overflow-x-auto my-6"><table class="min-w-full divide-y divide-border border border-border"><thead><tr><th class="px-4 py-2 text-left text-sm font-semibold text-foreground bg-muted/50">user: User</th><th class="px-4 py-2 text-left text-sm font-semibold text-foreground bg-muted/50">null;</th></tr></thead><tbody class="divide-y divide-border"></tbody></table></div>
    notifications: Notification[];
    }

    @Injectable({ providedIn: "root" })
    export class StateService {
    // Private writable signals
    <div class="overflow-x-auto my-6"><table class="min-w-full divide-y divide-border border border-border"><thead><tr><th class="px-4 py-2 text-left text-sm font-semibold text-foreground bg-muted/50">private _user = signal&lt;User</th><th class="px-4 py-2 text-left text-sm font-semibold text-foreground bg-muted/50">null&gt;(null);</th></tr></thead><tbody class="divide-y divide-border"></tbody></table></div>
    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]);
    }
    }

    Component Store Pattern with Signals

    import { 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);
    }
    }


    9. Forms with Signals (Coming in v22+)

    Current Reactive Forms

    import { 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);
    }
    }
    }

    Signal-Aware Form Patterns (Preview)

    // 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() });
    }
    }
    }


    10. Performance Optimization

    Change Detection Strategies

    @Component({
    changeDetection: ChangeDetectionStrategy.OnPush,
    // Only checks when:
    // 1. Input signal/reference changes
    // 2. Event handler runs
    // 3. Async pipe emits
    // 4. Signal value changes
    })

    Defer Blocks for Lazy Loading

    @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>
    }

    })

    NgOptimizedImage

    import { 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"
    />

    })


    11. Testing Modern Angular

    Testing Signal Components

    import { 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");
    });
    });

    Testing with Signal Inputs

    import { 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");
    });
    });


    Best Practices Summary

    Pattern✅ Do❌ Don't
    StateUse Signals for local stateOveruse RxJS for simple state
    ComponentsStandalone with direct importsBloated SharedModules
    Change DetectionOnPush + SignalsDefault CD everywhere
    Lazy Loading@defer and loadComponentEager load everything
    DIinject() functionConstructor injection (verbose)
    Inputsinput() signal function@Input() decorator (legacy)
    ZonelessEnable for new projectsForce on legacy without testing


    Resources

  • Angular.dev Documentation

  • Angular Signals Guide

  • Angular SSR Guide

  • Angular Update Guide

  • Angular Blog

  • Common Troubleshooting

    IssueSolution
    Signal not updating UIEnsure OnPush + call signal as function count()
    Hydration mismatchCheck server/client content consistency
    Circular dependencyUse inject() with forwardRef
    Zoneless not detecting changesTrigger via signal updates, not mutations
    SSR fetch failsUse TransferState or withFetch()