Angular Signals give the framework a fine-grained reactivity model — signal() creates a reactive value, computed() derives values lazily, and effect() runs side effects when dependencies change. Components mark only the DOM nodes that depend on changed signals for update, replacing Angular’s full component tree change detection. Zoneless mode (provideZonelessChangeDetection()) removes Zone.js entirely, reducing bundle size and enabling native async/await without patching. input() and output() declare component APIs as signals. resource() loads async data with built-in loading/error states. toSignal() bridges existing RxJS observables to the signals world. Claude Code generates Angular signal-based components, services, route resolvers, interceptors, and the zoneless application configurations for production Angular 19+ applications.
CLAUDE.md for Angular Signals
## Angular Signals Stack
- Version: @angular/core >= 19, standalone components (no NgModules)
- Reactivity: signal(), computed(), effect() — core primitives
- Interop: toSignal(observable$) to consume RxJS; toObservable(sig) for reverse
- Component API: input() / output() replaces @Input / @Output decorators
- Async: resource() for async reads; linkedSignal() for dependent state
- Zoneless: provideZonelessChangeDetection() in app.config.ts — remove zone.js from polyfills
- State: inject(SignalStore) pattern with signalStore() from NgRx Signals
- HTTP: inject(HttpClient) — returns Observable, wrap with toSignal() or rxResource()
Signal-Based Component
// src/app/components/order-list/order-list.component.ts
import {
Component, inject, signal, computed, effect, ChangeDetectionStrategy
} from "@angular/core"
import { toSignal } from "@angular/core/rxjs-interop"
import { FormsModule } from "@angular/forms"
import { JsonPipe, CurrencyPipe, DatePipe } from "@angular/common"
import { OrderService } from "../../services/order.service"
@Component({
selector: "app-order-list",
standalone: true,
imports: [FormsModule, CurrencyPipe, DatePipe],
changeDetection: ChangeDetectionStrategy.OnPush, // Signals-compatible
template: `
<div class="order-list">
<div class="stats">
<span>{{ filteredOrders().length }} orders</span>
<span>Total: {{ totalSpent() | currency }}</span>
</div>
<div class="filters">
<input
type="search"
[value]="searchQuery()"
(input)="searchQuery.set($event.target.value)"
placeholder="Search orders..."
/>
<select
[value]="selectedStatus()"
(change)="selectedStatus.set($event.target.value)"
>
<option value="">All statuses</option>
<option value="pending">Pending</option>
<option value="shipped">Shipped</option>
</select>
</div>
@if (ordersResource.isLoading()) {
<p>Loading...</p>
} @else if (ordersResource.error()) {
<p class="error">{{ ordersResource.error() }}</p>
} @else {
@for (order of filteredOrders(); track order.id) {
<div class="order-card">
<span>{{ order.id | slice:0:8 }}</span>
<span>{{ order.status }}</span>
<span>{{ order.totalCents / 100 | currency }}</span>
<span>{{ order.createdAt | date:'short' }}</span>
</div>
} @empty {
<p>No orders found</p>
}
}
</div>
`,
})
export class OrderListComponent {
private orderService = inject(OrderService)
// Mutable state signals
searchQuery = signal("")
selectedStatus = signal("")
// resource(): async data with loading/error states (Angular 19+)
ordersResource = this.orderService.ordersResource()
// computed(): derived state — re-computes only when dependencies change
filteredOrders = computed(() => {
const orders = this.ordersResource.value() ?? []
const query = this.searchQuery().toLowerCase()
const status = this.selectedStatus()
return orders.filter(order => {
const matchesQuery = !query || order.id.includes(query)
const matchesStatus = !status || order.status === status
return matchesQuery && matchesStatus
})
})
totalSpent = computed(() =>
this.filteredOrders().reduce((sum, o) => sum + o.totalCents, 0) / 100
)
constructor() {
// effect(): runs when dependencies change, after render
effect(() => {
const count = this.filteredOrders().length
// Track for analytics — runs whenever filteredOrders changes
if (count > 0) {
console.log(`Displaying ${count} orders`)
}
})
}
}
Signal-Based Service
// src/app/services/order.service.ts
import { Injectable, inject, signal, resource } from "@angular/core"
import { HttpClient } from "@angular/common/http"
import { toSignal } from "@angular/core/rxjs-interop"
import { map } from "rxjs/operators"
export interface Order {
id: string
customerId: string
status: "pending" | "processing" | "shipped" | "delivered" | "cancelled"
totalCents: number
createdAt: string
}
@Injectable({ providedIn: "root" })
export class OrderService {
private http = inject(HttpClient)
private _customerId = signal<string | null>(null)
setCustomer(id: string) {
this._customerId.set(id)
}
// resource(): reactive async loader — re-fetches when customerId changes
ordersResource() {
return resource({
// Request signal: re-runs loader when this changes
request: () => this._customerId(),
loader: async ({ request: customerId, abortSignal }) => {
if (!customerId) return []
const response = await fetch(`/api/orders?customerId=${customerId}`, {
signal: abortSignal,
})
if (!response.ok) throw new Error(`HTTP ${response.status}`)
return response.json() as Promise<Order[]>
},
})
}
// Convert RxJS observable to signal
orderStats = toSignal(
this.http.get<{ total: number; revenue: number }>("/api/orders/stats"),
{ initialValue: { total: 0, revenue: 0 } }
)
async cancelOrder(orderId: string): Promise<void> {
await fetch(`/api/orders/${orderId}/cancel`, { method: "POST" })
}
}
Signal Inputs and Outputs
// src/app/components/order-card/order-card.component.ts
import {
Component, input, output, computed, ChangeDetectionStrategy
} from "@angular/core"
import { CurrencyPipe } from "@angular/common"
@Component({
selector: "app-order-card",
standalone: true,
imports: [CurrencyPipe],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<div class="card" [class.highlighted]="isHighlighted()">
<h3>Order #{{ order().id | slice:0:8 }}</h3>
<p>{{ order().status }}</p>
<p>{{ order().totalCents / 100 | currency }}</p>
@if (order().status === 'pending') {
<button (click)="handleCancel()">Cancel</button>
}
</div>
`,
})
export class OrderCardComponent {
// input() replaces @Input() — returns a signal
order = input.required<Order>()
highlightedId = input<string | null>(null)
// output() replaces @Output() EventEmitter
cancelled = output<string>()
selected = output<Order>()
// computed from input signals
isHighlighted = computed(() => this.order().id === this.highlightedId())
handleCancel() {
this.cancelled.emit(this.order().id)
}
}
Zoneless Configuration
// src/app/app.config.ts — zoneless Angular 19 app
import {
ApplicationConfig, provideZonelessChangeDetection
} from "@angular/core"
import { provideRouter, withComponentInputBinding } from "@angular/router"
import { provideHttpClient, withFetch, withInterceptors } from "@angular/common/http"
import { authInterceptor } from "./interceptors/auth.interceptor"
import { routes } from "./app.routes"
export const appConfig: ApplicationConfig = {
providers: [
// Zoneless change detection — removes Zone.js requirement
provideZonelessChangeDetection(),
provideRouter(routes, withComponentInputBinding()),
provideHttpClient(
withFetch(),
withInterceptors([authInterceptor]),
),
],
}
// src/app/interceptors/auth.interceptor.ts
import {
HttpInterceptorFn, HttpRequest, HttpHandlerFn
} from "@angular/common/http"
import { inject } from "@angular/core"
import { AuthService } from "../services/auth.service"
export const authInterceptor: HttpInterceptorFn = (
req: HttpRequest<unknown>,
next: HttpHandlerFn
) => {
const auth = inject(AuthService)
const token = auth.token() // auth.token is a signal
if (token) {
const authReq = req.clone({
headers: req.headers.set("Authorization", `Bearer ${token}`),
})
return next(authReq)
}
return next(req)
}
NgRx Signal Store
// src/app/store/order.store.ts — NgRx Signals store
import { signalStore, withState, withComputed, withMethods } from "@ngrx/signals"
import { computed, inject } from "@angular/core"
import { OrderService } from "../services/order.service"
interface OrderState {
orders: Order[]
loading: boolean
error: string | null
selectedId: string | null
}
export const OrderStore = signalStore(
{ providedIn: "root" },
withState<OrderState>({
orders: [],
loading: false,
error: null,
selectedId: null,
}),
withComputed(({ orders, selectedId }) => ({
selectedOrder: computed(() =>
orders().find(o => o.id === selectedId())
),
pendingOrders: computed(() =>
orders().filter(o => o.status === "pending")
),
totalRevenue: computed(() =>
orders().reduce((sum, o) => sum + o.totalCents, 0) / 100
),
})),
withMethods((store, orderService = inject(OrderService)) => ({
async loadOrders(customerId: string) {
patchState(store, { loading: true, error: null })
try {
const orders = await orderService.fetchOrders(customerId)
patchState(store, { orders, loading: false })
} catch (e) {
patchState(store, { error: (e as Error).message, loading: false })
}
},
selectOrder(id: string) {
patchState(store, { selectedId: id })
},
}))
)
For the React equivalent fine-grained reactivity model with Zustand or Jotai atom-based state that Angular signals are conceptually similar to, see the React state management guide for atomic state patterns. For the Vue 3 Composition API with ref() and computed() that inspired Angular’s signal design, the Vue guide covers reactivity patterns. The Claude Skills 360 bundle includes Angular Signals skill sets covering reactivity primitives, zoneless setup, and NgRx Signals store. Start with the free tier to try Angular signal component generation.