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
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
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
:
`
{{ name() }}
{{ role() }}` , } ) 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: //
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 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 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 : `
,
}
)
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
:
- `
- ,
- }
- )
- 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 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 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 : `
` } ) 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 { 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 ] ) ; } } 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 : `
` , } ) 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 : `
@defer (on viewport) {
} @loading (minimum 200ms) {
Failed to load chart
}
}
)
NgOptimizedImage
import
{
NgOptimizedImage
}
from
'@angular/common'
;
@
Component
(
{
imports
:
[
NgOptimizedImage
]
,
template
:
`
}
)
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 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 Resources Angular.dev Documentation Angular Signals Guide Angular SSR Guide Angular Update Guide Angular Blog Common Troubleshooting 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() Limitations Use this skill only when the task clearly matches the scope described above. Do not treat the output as a substitute for environment-specific validation, testing, or expert review. Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.