home / skills / analogjs / angular-skills / angular-signals
This skill helps Angular developers adopt signals for reactive state management, enabling writable, derived, and dependent state with effects.
npx playbooks add skill analogjs/angular-skills --skill angular-signalsReview the files below or copy the command above to add this skill to your agents.
---
name: angular-signals
description: Implement signal-based reactive state management in Angular v20+. Use for creating reactive state with signal(), derived state with computed(), dependent state with linkedSignal(), and side effects with effect(). Triggers on state management questions, converting from BehaviorSubject/Observable patterns to signals, or implementing reactive data flows.
---
# Angular Signals
Signals are Angular's reactive primitive for state management. They provide synchronous, fine-grained reactivity.
## Core Signal APIs
### signal() - Writable State
```typescript
import { signal } from '@angular/core';
// Create writable signal
const count = signal(0);
// Read value
console.log(count()); // 0
// Set new value
count.set(5);
// Update based on current value
count.update(c => c + 1);
// With explicit type
const user = signal<User | null>(null);
user.set({ id: 1, name: 'Alice' });
```
### computed() - Derived State
```typescript
import { signal, computed } from '@angular/core';
const firstName = signal('John');
const lastName = signal('Doe');
// Derived signal - automatically updates when dependencies change
const fullName = computed(() => `${firstName()} ${lastName()}`);
console.log(fullName()); // "John Doe"
firstName.set('Jane');
console.log(fullName()); // "Jane Doe"
// Computed with complex logic
const items = signal<Item[]>([]);
const filter = signal('');
const filteredItems = computed(() => {
const query = filter().toLowerCase();
return items().filter(item =>
item.name.toLowerCase().includes(query)
);
});
const totalPrice = computed(() =>
filteredItems().reduce((sum, item) => sum + item.price, 0)
);
```
### linkedSignal() - Dependent State with Reset
```typescript
import { signal, linkedSignal } from '@angular/core';
const options = signal(['A', 'B', 'C']);
// Resets to first option when options change
const selected = linkedSignal(() => options()[0]);
console.log(selected()); // "A"
selected.set('B'); // User selects B
console.log(selected()); // "B"
options.set(['X', 'Y']); // Options change
console.log(selected()); // "X" - auto-reset to first
// With previous value access
const items = signal<Item[]>([]);
const selectedItem = linkedSignal<Item[], Item | null>({
source: () => items(),
computation: (newItems, previous) => {
// Try to preserve selection if item still exists
const prevItem = previous?.value;
if (prevItem && newItems.some(i => i.id === prevItem.id)) {
return prevItem;
}
return newItems[0] ?? null;
},
});
```
### effect() - Side Effects
```typescript
import { signal, effect, inject, DestroyRef } from '@angular/core';
@Component({...})
export class SearchComponent {
query = signal('');
constructor() {
// Effect runs when query changes
effect(() => {
console.log('Search query:', this.query());
});
// Effect with cleanup
effect((onCleanup) => {
const timer = setInterval(() => {
console.log('Current query:', this.query());
}, 1000);
onCleanup(() => clearInterval(timer));
});
}
}
```
**Effect rules:**
- Cannot write to signals by default (use `allowSignalWrites` if needed)
- Run in injection context (constructor or with `runInInjectionContext`)
- Automatically cleaned up when component destroys
```typescript
// Writing signals in effects (use sparingly)
effect(() => {
if (this.query().length > 0) {
this.hasSearched.set(true);
}
}, { allowSignalWrites: true });
```
## Component State Pattern
```typescript
@Component({
selector: 'app-todo-list',
template: `
<input [value]="newTodo()" (input)="newTodo.set($any($event.target).value)" />
<button (click)="addTodo()" [disabled]="!canAdd()">Add</button>
<ul>
@for (todo of filteredTodos(); track todo.id) {
<li [class.done]="todo.done">
{{ todo.text }}
<button (click)="toggleTodo(todo.id)">Toggle</button>
</li>
}
</ul>
<p>{{ remaining() }} remaining</p>
`,
})
export class TodoListComponent {
// State
todos = signal<Todo[]>([]);
newTodo = signal('');
filter = signal<'all' | 'active' | 'done'>('all');
// Derived state
canAdd = computed(() => this.newTodo().trim().length > 0);
filteredTodos = computed(() => {
const todos = this.todos();
switch (this.filter()) {
case 'active': return todos.filter(t => !t.done);
case 'done': return todos.filter(t => t.done);
default: return todos;
}
});
remaining = computed(() =>
this.todos().filter(t => !t.done).length
);
// Actions
addTodo() {
const text = this.newTodo().trim();
if (text) {
this.todos.update(todos => [
...todos,
{ id: crypto.randomUUID(), text, done: false }
]);
this.newTodo.set('');
}
}
toggleTodo(id: string) {
this.todos.update(todos =>
todos.map(t => t.id === id ? { ...t, done: !t.done } : t)
);
}
}
```
## RxJS Interop
### toSignal() - Observable to Signal
```typescript
import { toSignal } from '@angular/core/rxjs-interop';
import { interval } from 'rxjs';
@Component({...})
export class TimerComponent {
private http = inject(HttpClient);
// From observable - requires initial value or allowUndefined
counter = toSignal(interval(1000), { initialValue: 0 });
// From HTTP - undefined until loaded
users = toSignal(this.http.get<User[]>('/api/users'));
// With requireSync for synchronous observables (BehaviorSubject)
private user$ = new BehaviorSubject<User | null>(null);
currentUser = toSignal(this.user$, { requireSync: true });
}
```
### toObservable() - Signal to Observable
```typescript
import { toObservable } from '@angular/core/rxjs-interop';
import { switchMap, debounceTime } from 'rxjs';
@Component({...})
export class SearchComponent {
query = signal('');
private http = inject(HttpClient);
// Convert signal to observable for RxJS operators
results = toSignal(
toObservable(this.query).pipe(
debounceTime(300),
switchMap(q => this.http.get<Result[]>(`/api/search?q=${q}`))
),
{ initialValue: [] }
);
}
```
## Signal Equality
```typescript
// Custom equality function
const user = signal<User>(
{ id: 1, name: 'Alice' },
{ equal: (a, b) => a.id === b.id }
);
// Only triggers updates when ID changes
user.set({ id: 1, name: 'Alice Updated' }); // No update
user.set({ id: 2, name: 'Bob' }); // Triggers update
```
## Untracked Reads
```typescript
import { untracked } from '@angular/core';
const a = signal(1);
const b = signal(2);
// Only depends on 'a', not 'b'
const result = computed(() => {
const aVal = a();
const bVal = untracked(() => b());
return aVal + bVal;
});
```
## Service State Pattern
```typescript
@Injectable({ providedIn: 'root' })
export class AuthService {
// Private writable state
private _user = signal<User | null>(null);
private _loading = signal(false);
// Public read-only signals
readonly user = this._user.asReadonly();
readonly loading = this._loading.asReadonly();
readonly isAuthenticated = computed(() => this._user() !== null);
private http = inject(HttpClient);
async login(credentials: Credentials): Promise<void> {
this._loading.set(true);
try {
const user = await firstValueFrom(
this.http.post<User>('/api/login', credentials)
);
this._user.set(user);
} finally {
this._loading.set(false);
}
}
logout(): void {
this._user.set(null);
}
}
```
For advanced patterns including resource(), see [references/signal-patterns.md](references/signal-patterns.md).
This skill implements signal-based reactive state management for Angular v20+. It teaches creating writable signals with signal(), derived values with computed(), dependent resets with linkedSignal(), and side effects with effect(). The skill focuses on converting BehaviorSubject/Observable patterns to signals and building predictable, synchronous state flows.
The skill inspects component and service state patterns and shows how to replace RxJS-centric state with Angular signals. It demonstrates creating signals, composing computed and linkedSignal dependents, and using effect for side effects and cleanup. It also covers RxJS interop with toSignal and toObservable plus equality, untracked reads, and readonly state for services.
Can effects write to signals?
By default effects cannot write to signals. Use the allowSignalWrites option sparingly when you need an effect to update other signals.
How do I convert a BehaviorSubject to a signal without losing the current value?
Use toSignal(behaviorSubject, { requireSync: true }) to create a signal that immediately reflects the BehaviorSubject's current value.
When should I use linkedSignal vs computed?
Use computed for derived values that always follow dependencies. Use linkedSignal when you need a dependent value that can be set independently but should reset or recompute when its source changes.