Angular Signals
Angular Signals for fine-grained reactivity and efficient change detection
You are an expert in Angular Signals for building reactive Angular applications with fine-grained reactivity. ## Key Points - Logging and analytics - Syncing with `localStorage` - Custom DOM manipulation not covered by the template - Third-party library integration 1. **Prefer `computed` over `effect` for derived state.** Computed signals are lazy, memoized, and declarative. Effects are imperative and harder to reason about. 2. **Expose read-only signals from services.** Use `asReadonly()` to prevent external code from mutating state directly: 3. **Use `input()` and `model()` instead of `@Input()` and `@Output()`.** Signal-based inputs enable better type safety and remove the need for `ngOnChanges`. 4. **Use `toSignal` with an `initialValue`.** Without one, the signal type includes `undefined`, which complicates downstream code. 5. **Keep effects focused.** Each effect should handle a single responsibility. Avoid reading many signals in one effect. 6. **Avoid writing to signals inside `computed`.** Computed callbacks must be pure — no side effects. - **Reading signals outside a reactive context.** Calling a signal inside `setTimeout` or a promise callback will not track dependencies. Use `effect` or `toObservable` for async scenarios. - **Creating effects outside an injection context.** `effect()` must be called in a constructor or inside `runInInjectionContext`. Otherwise it throws a runtime error. ## Quick Example ```html <app-toggle [(checked)]="isEnabled" /> ``` ```typescript private _data = signal<Data[]>([]); readonly data = this._data.asReadonly(); ```
skilldb get angular-skills/Angular SignalsFull skill: 258 linesAngular Signals — Angular
You are an expert in Angular Signals for building reactive Angular applications with fine-grained reactivity.
Core Philosophy
Angular Signals represent the framework's shift from zone.js-driven change detection to fine-grained reactivity. The core idea is simple: a signal is a value that notifies consumers when it changes. But the implications are profound — signals enable Angular to know exactly which parts of the template need updating, eliminating the need for top-down dirty checking. This makes applications faster by default and removes an entire category of performance debugging (unnecessary change detection cycles).
The signal API is deliberately minimal: signal() creates writable state, computed() derives values, and effect() handles side effects. This three-primitive model maps directly to the reactive programming fundamentals of state, derivation, and side effects. The hierarchy matters: computed should vastly outnumber effect in any codebase. If you find yourself writing many effects that update other signals, you are likely doing derived state computation that belongs in computed. Effects are for the edges of the system — syncing with localStorage, logging, integrating with third-party libraries.
Signal inputs (input()) and model signals (model()) extend the reactive model to component boundaries. They replace @Input() and @Output() with a unified, signal-based API that eliminates ngOnChanges, enables computed derivations from inputs, and provides better type inference. Adopting signal inputs is not just a syntax preference — it fundamentally simplifies how components react to input changes.
Anti-Patterns
-
Using
effect()for Derived State — writingeffect(() => { this.total.set(this.price() * this.quantity()); })whencomputed(() => this.price() * this.quantity())is simpler, lazy, memoized, and does not require cleanup. -
Mutating Signal Values In Place — modifying an object inside a signal without creating a new reference, then expecting the UI to update. Signals use referential equality by default, so
update(arr => { arr.push(item); return arr; })does not trigger because the same array reference is returned. -
Effects Outside Injection Context — calling
effect()inside asetTimeout, promise.then(), or standalone function where no injection context exists. Effects must be created in constructors, field initializers, or withinrunInInjectionContext. -
Reading Signals in Non-Reactive Contexts — calling a signal inside
setTimeoutor a promise callback expecting it to track dependencies. Signal reads only create reactive subscriptions insidecomputed,effect, or Angular's template rendering context. -
Forgetting
untrackedin Effects — reading a signal inside aneffectthat you do not want to track as a dependency. Withoutuntracked(() => sig()), every signal read creates a dependency that re-triggers the effect on change.
Overview
Angular Signals, introduced in Angular 16 and stabilized in Angular 17+, provide a synchronous, fine-grained reactivity primitive that tracks how and where state is used throughout an application. Signals replace many use cases for RxJS-based state management and zone.js-driven change detection with a simpler, more predictable model.
Core Concepts
Writable Signals
A writable signal holds a value and exposes methods to update it:
import { signal } from '@angular/core';
// Create a writable signal with an initial value
const count = signal(0);
// Read the current value by calling the signal
console.log(count()); // 0
// Set a new value
count.set(5);
// Update based on the previous value
count.update(prev => prev + 1);
Computed Signals
Computed signals derive values from other signals. They are lazy and memoized — they only recompute when a dependency changes and only when read:
import { signal, computed } from '@angular/core';
const firstName = signal('Jane');
const lastName = signal('Doe');
const fullName = computed(() => `${firstName()} ${lastName()}`);
console.log(fullName()); // "Jane Doe"
firstName.set('John');
console.log(fullName()); // "John Doe"
Effects
Effects run side effects whenever the signals they read change. They execute at least once and then re-execute when any dependency signal changes:
import { signal, effect } from '@angular/core';
const query = signal('');
effect(() => {
console.log(`Search query changed: ${query()}`);
});
Effects are primarily intended for:
- Logging and analytics
- Syncing with
localStorage - Custom DOM manipulation not covered by the template
- Third-party library integration
Signal Inputs
Angular 17.1+ introduced signal-based inputs for components:
import { Component, input } from '@angular/core';
@Component({
selector: 'app-user-card',
template: `<h2>{{ name() }}</h2><p>{{ bio() }}</p>`,
})
export class UserCardComponent {
name = input.required<string>();
bio = input<string>('No bio provided');
}
Model Signals (Two-Way Binding)
Angular 17.2+ added the model() function for two-way binding:
import { Component, model } from '@angular/core';
@Component({
selector: 'app-toggle',
template: `<button (click)="checked.set(!checked())">{{ checked() ? 'ON' : 'OFF' }}</button>`,
})
export class ToggleComponent {
checked = model(false);
}
Used in a parent:
<app-toggle [(checked)]="isEnabled" />
Implementation Patterns
Signal Store Pattern
A lightweight store built with signals, useful for component or feature-level state:
import { Injectable, signal, computed } from '@angular/core';
interface Todo {
id: number;
title: string;
completed: boolean;
}
@Injectable({ providedIn: 'root' })
export class TodoStore {
private readonly _todos = signal<Todo[]>([]);
readonly todos = this._todos.asReadonly();
readonly completedCount = computed(() =>
this._todos().filter(t => t.completed).length
);
readonly pendingCount = computed(() =>
this._todos().filter(t => !t.completed).length
);
addTodo(title: string): void {
this._todos.update(todos => [
...todos,
{ id: Date.now(), title, completed: false },
]);
}
toggleTodo(id: number): void {
this._todos.update(todos =>
todos.map(t => (t.id === id ? { ...t, completed: !t.completed } : t))
);
}
removeTodo(id: number): void {
this._todos.update(todos => todos.filter(t => t.id !== id));
}
}
Converting Between Signals and Observables
Angular provides interop functions in @angular/core/rxjs-interop:
import { signal } from '@angular/core';
import { toObservable, toSignal } from '@angular/core/rxjs-interop';
import { HttpClient } from '@angular/common/http';
import { switchMap } from 'rxjs';
@Component({ /* ... */ })
export class SearchComponent {
query = signal('');
private query$ = toObservable(this.query);
results = toSignal(
this.query$.pipe(
switchMap(q => this.http.get<Result[]>(`/api/search?q=${q}`))
),
{ initialValue: [] }
);
constructor(private http: HttpClient) {}
}
Signal-Based Component with OnPush
Signals work seamlessly with OnPush change detection, and in Angular 18+ with the new zoneless change detection:
@Component({
selector: 'app-counter',
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ doubled() }}</p>
<button (click)="increment()">+1</button>
`,
})
export class CounterComponent {
count = signal(0);
doubled = computed(() => this.count() * 2);
increment(): void {
this.count.update(c => c + 1);
}
}
Best Practices
-
Prefer
computedovereffectfor derived state. Computed signals are lazy, memoized, and declarative. Effects are imperative and harder to reason about. -
Expose read-only signals from services. Use
asReadonly()to prevent external code from mutating state directly:private _data = signal<Data[]>([]); readonly data = this._data.asReadonly(); -
Use
input()andmodel()instead of@Input()and@Output(). Signal-based inputs enable better type safety and remove the need forngOnChanges. -
Use
toSignalwith aninitialValue. Without one, the signal type includesundefined, which complicates downstream code. -
Keep effects focused. Each effect should handle a single responsibility. Avoid reading many signals in one effect.
-
Avoid writing to signals inside
computed. Computed callbacks must be pure — no side effects.
Common Pitfalls
-
Reading signals outside a reactive context. Calling a signal inside
setTimeoutor a promise callback will not track dependencies. UseeffectortoObservablefor async scenarios. -
Creating effects outside an injection context.
effect()must be called in a constructor or insiderunInInjectionContext. Otherwise it throws a runtime error. -
Overusing effects for state derivation. Writing to a signal inside an effect to derive state creates unnecessary complexity. Use
computedinstead. -
Forgetting
untrackedin effects. If an effect reads a signal it should not track, wrap the read inuntracked(() => sig())to avoid unwanted re-executions. -
Mutating objects inside signals. Signals use referential equality by default. Mutating an object in place and calling
set()with the same reference will not trigger updates. Always create a new reference.
Install this skill directly: skilldb add angular-skills
Related Skills
Angular Testing
Testing Angular applications with Jest for unit tests and Cypress for end-to-end tests
Angular Dependency Injection
Angular dependency injection system including providers, injection tokens, and hierarchical injectors
Angular Forms
Reactive forms, form validation, dynamic forms, and typed form controls in Angular
Angular Ngrx
NgRx state management with store, effects, selectors, and the component store
Angular Routing
Angular Router configuration including lazy loading, guards, resolvers, and nested routes
Angular Rxjs Patterns
RxJS reactive patterns for data fetching, state management, and event handling in Angular