Covers Angular Signals in depth: the full reactive primitives system (signal, computed, effect, linkedSignal, resource, rxResource), signal-based component APIs (input, output, model, viewChild, viewChildren, contentChild, contentChildren), zoneless change detection and Zone.js migration, RxJS interop (toSignal, toObservable), state management patterns, Signal Forms, and signal diagnostics. Use for \"Angular signals\", \"signal()\", \"computed()\", \"effect()\", \"linkedSignal\", \"resource a...
Pro scans all 3 files and shows the line behind each finding
Scanned 9/24/2026
npx -y skills add chrishuffman5/domain-expert --skill angular-signals --agent claude-codeInstalls into .claude/skills of the current project.
Are you the author of Angular Signals?
Add the live security badge to your README — it updates automatically with every re-scan.
[](https://www.skillsdirectory.com/skills/chrishuffman5-angular-signals)More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.
---
name: angular-signals
description: "Covers Angular Signals in depth: the full reactive primitives system (signal, computed, effect, linkedSignal, resource, rxResource), signal-based component APIs (input, output, model, viewChild, viewChildren, contentChild, contentChildren), zoneless change detection and Zone.js migration, RxJS interop (toSignal, toObservable), state management patterns, Signal Forms, and signal diagnostics. Use for \"Angular signals\", \"signal()\", \"computed()\", \"effect()\", \"linkedSignal\", \"resource angular\", \"rxResource\", \"toSignal\", \"toObservable\", \"signal input\", \"signal query\", \"viewChild signal\", \"zoneless\", \"zoneless migration\", \"Zone.js removal\", \"signal forms\" questions. Do NOT use for general Angular questions (DI, routing, template syntax, RxJS) that don't involve signals -- use the `angular` skill."
license: MIT
---
# Angular Signals
This skill covers Angular's Signals system -- the reactive primitives that power modern Angular applications -- across the full signal lifecycle from v16 (developer preview) through v21 (zoneless default), including:
- Core primitives: `signal()`, `computed()`, `effect()`, `linkedSignal()` (v19+), `resource()`/`rxResource()` (v19+)
- Signal-based component APIs: `input()`, `output()`, `model()`, `viewChild()`, `viewChildren()`, `contentChild()`, `contentChildren()`
- Zoneless change detection: how it works, migration from Zone.js, hybrid strategies
- RxJS interop: `toSignal()`, `toObservable()`, bridging patterns
- State management: signal stores, service patterns, comparison with NgRx
- Signal Forms: experimental form primitives built on signals (v21)
- Diagnostics: common pitfalls, debugging techniques, Angular DevTools signal inspector
## How to Approach Tasks
1. **Classify** the request:
- **Primitives / API** -- Load `references/architecture.md`
- **Zoneless Migration** -- Load `references/migration.md`
- **RxJS Interop** -- Load `references/migration.md` (interop section)
- **State Management** -- Use signal store patterns in this file
- **Troubleshooting** -- Use the diagnostics section below
2. **Identify version** -- Signal API stability varies by version:
- v19: `linkedSignal`, `resource()` experimental
- v20: all signal APIs stable
- v21: zoneless default, Signal Forms experimental
3. **Load context** -- Read the relevant reference file for deep knowledge.
4. **Recommend** -- Provide signal-idiomatic solutions. Prefer `computed()` over `effect()` for derived state, `linkedSignal()` over manual reset patterns, `resource()` over effects with HTTP calls.
## Core Primitives
### signal() -- Writable Reactive State
```typescript
import { signal } from '@angular/core';
const count = signal(0);
count(); // read: 0
count.set(5); // replace
count.update(n => n + 1); // derive from current
```
Custom equality suppresses unnecessary notifications:
```typescript
const user = signal<User>(
{ id: 1, name: 'Alice' },
{ equal: (a, b) => a.id === b.id }
);
```
Read-only view for service APIs:
```typescript
private _items = signal<CartItem[]>([]);
readonly items = this._items.asReadonly();
```
### computed() -- Derived Reactive State
Lazy, cached, read-only. Recalculates only when signal dependencies change.
```typescript
const firstName = signal('Jane');
const lastName = signal('Doe');
const fullName = computed(() => `${firstName()} ${lastName()}`);
```
Dependencies are tracked per execution path -- conditional reads create dynamic dependency graphs.
**When NOT to use computed():**
- Side effects needed (use `effect()`)
- Write access needed (use `linkedSignal()`)
- Depends on non-signal external state
### effect() -- Side Effects
Runs once immediately, then re-runs when signal dependencies change.
```typescript
effect(() => {
document.body.className = theme();
});
```
Must be called in an injection context (constructor/field initializer) or with an explicit injector. Use `onCleanup` for teardown.
```typescript
effect((onCleanup) => {
const sub = interval(1000).subscribe(() => console.log(tick()));
onCleanup(() => sub.unsubscribe());
});
```
**Do not write signals in effects** -- use `computed()` or `linkedSignal()` for derived state. **Do not make API calls in effects** -- use `resource()`.
### linkedSignal() -- Writable Derived State (v19+)
A writable signal that auto-resets when a source signal changes.
```typescript
const country = signal('US');
const city = linkedSignal(() => getDefaultCity(country()));
city.set('Boston'); // writable override
country.set('CA'); // city resets to default for CA
```
Options object form for preserving previous values:
```typescript
const selected = linkedSignal({
source: options,
computation: (newOptions, previous) => {
if (previous && newOptions.includes(previous.value)) return previous.value;
return newOptions[0];
},
});
```
### resource() and rxResource() -- Async Data (v19+)
```typescript
productResource = resource({
request: this.productId,
loader: async ({ request: id, abortSignal }) => {
const res = await fetch(`/api/products/${id}`, { signal: abortSignal });
return res.json();
}
});
```
```typescript
import { rxResource } from '@angular/core/rxjs-interop';
productResource = rxResource({
request: this.productId,
loader: ({ request: id }) => this.http.get<Product>(`/api/products/${id}`)
});
```
Status values: `idle`, `loading`, `reloading`, `resolved`, `error`, `local`.
## Signal-Based Component APIs
### input() and input.required()
```typescript
name = input<string>(''); // optional with default
userId = input.required<string>(); // required, no default
```
Returns `InputSignal<T>` -- read-only, reactive in computed/effect.
### model() -- Two-Way Binding
```typescript
isExpanded = model(false); // ModelSignal<boolean>
```
Parent: `<app-panel [(isExpanded)]="panelOpen" />`
### output()
```typescript
searched = output<string>();
this.searched.emit(query);
```
### viewChild(), viewChildren()
```typescript
canvas = viewChild<ElementRef<HTMLCanvasElement>>('canvas');
canvasRequired = viewChild.required<ElementRef<HTMLCanvasElement>>('canvas');
items = viewChildren(ItemComponent);
```
### contentChild(), contentChildren()
```typescript
icon = contentChild(IconComponent);
tabs = contentChildren(TabComponent);
```
All signal queries update reactively and work naturally in `computed()` and `effect()`.
## State Management with Signals
### Service with Signals (Recommended)
```typescript
@Injectable({ providedIn: 'root' })
export class CartService {
private _items = signal<CartItem[]>([]);
readonly items = this._items.asReadonly();
readonly total = computed(() => this._items().reduce((sum, i) => sum + i.price * i.qty, 0));
readonly count = computed(() => this._items().reduce((sum, i) => sum + i.qty, 0));
readonly isEmpty = computed(() => this._items().length === 0);
addItem(item: CartItem) {
this._items.update(items => {
const existing = items.find(i => i.id === item.id);
if (existing) {
return items.map(i => i.id === item.id ? { ...i, qty: i.qty + 1 } : i);
}
return [...items, { ...item, qty: 1 }];
});
}
removeItem(id: string) {
this._items.update(items => items.filter(i => i.id !== id));
}
clear() { this._items.set([]); }
}
```
### Comparison: Signals vs NgRx vs ComponentStore
| Concern | Signals | NgRx Store | ComponentStore |
|---|---|---|---|
| Boilerplate | Minimal | High | Medium |
| DevTools | Angular DevTools | Redux DevTools | Redux DevTools |
| Time travel | No | Yes | Yes |
| Best for | Local + simple shared state | Large-scale, audit-critical | Component-scoped complex state |
| Bundle impact | Zero (built-in) | ~50KB | ~15KB |
## Effect Patterns
### Legitimate Use Cases
| Use Case | Pattern |
|---|---|
| localStorage sync | Read signal, write to localStorage |
| Analytics tracking | Read signals, call analytics API |
| DOM manipulation | Canvas, video, third-party libraries |
| Third-party library sync | Migration pattern from Zone.js |
### Anti-Patterns
- Writing signals in effects (infinite loop risk)
- API calls in effects (use `resource()`)
- Derived state in effects (use `computed()`)
### untracked()
Read a signal without registering a dependency:
```typescript
effect(() => {
const log = auditLog(); // tracked dependency
const userId = untracked(() => currentUser()?.id); // not tracked
});
```
## Common Issues and Diagnostics
### Forgetting () in Templates
```html
<!-- WRONG -->
<span>{{ count }}</span>
<!-- CORRECT -->
<span>{{ count() }}</span>
```
### Writing Signals in computed()
`computed()` must be pure. Signal writes throw at runtime.
### Circular Effect Dependencies
Effects that write to signals read by other effects can create infinite loops. Angular detects some cycles and throws.
### Stale Values in Non-Reactive Context
Reading `doubled()` in a plain method captures the current value -- it is not reactive outside a reactive context (template, computed, effect).
### Memory Leaks from Manual toSignal()
`toSignal()` with `manualCleanup: true` requires explicit cleanup via `DestroyRef`.
### resource() with Null Request
When the request signal returns null/undefined, `resource()` enters idle state. The loader does not run. This is intentional but can be surprising.
### Third-Party Libraries After Removing Zone.js
Libraries calling Angular APIs from outside the zone will not trigger change detection. Fix: `markForCheck()` in the library callback.
## Version History
| Version | Signals Milestone |
|---|---|
| v16 | Developer Preview: `signal()`, `computed()`, `effect()` |
| v17 | Stable core primitives. Signal inputs/queries developer preview |
| v18 | `toSignal()`/`toObservable()` stable. NgRx Signal Store |
| v19 | `resource()`/`rxResource()` introduced. `linkedSignal()` stable. Zoneless stable API |
| v20 | All signal APIs stable. Signal inputs/queries stable. Full signal lifecycle |
| v21 | Zoneless default. Signal Forms (experimental). Migration schematic |
## Reference Files
- `references/architecture.md` -- Signal primitives, computed, effect, linkedSignal, resource, signal inputs/queries
- `references/migration.md` -- Zone.js to zoneless migration, RxJS interop patterns
Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.
No comments yet. Be the first to comment!