angular-signals

Signals are Angular's reactive primitive for state management. They provide synchronous, fine-grained reactivity.

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "angular-signals" with this command: npx skills add zard-ui/zardui/zard-ui-zardui-angular-signals

Angular Signals

Signals are Angular's reactive primitive for state management. They provide synchronous, fine-grained reactivity.

Core Signal APIs

signal() - Writable State

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

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

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

import { signal, effect, inject, DestroyRef } from '@angular/core';

@Component({...}) export class Search { 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:

  • Run in injection context (constructor or with runInInjectionContext )

  • Automatically cleaned up when component destroys

Component State Pattern

@Component({ selector: 'app-todo-list', template: ` <input [value]="newTodo()" (input)="newTodo.set($any($event.target).value)" /> <button (click)="addTodo()" [disabled]="!canAdd()">Add</button>

&#x3C;ul>
  @for (todo of filteredTodos(); track todo.id) {
    &#x3C;li [class.done]="todo.done">
      {{ todo.text }}
      &#x3C;button (click)="toggleTodo(todo.id)">Toggle&#x3C;/button>
    &#x3C;/li>
  }
&#x3C;/ul>

&#x3C;p>{{ remaining() }} remaining&#x3C;/p>

`, }) export class TodoList { // 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

import { toSignal } from '@angular/core/rxjs-interop'; import { interval } from 'rxjs';

@Component({...}) export class Timer { 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

import { toObservable } from '@angular/core/rxjs-interop'; import { switchMap, debounceTime } from 'rxjs';

@Component({...}) export class Search { 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

// 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

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

@Injectable({ providedIn: 'root' }) export class Auth { // 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.

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

General

angular-di

No summary provided by upstream source.

Repository SourceNeeds Review
General

angular-testing

No summary provided by upstream source.

Repository SourceNeeds Review
General

angular-http

No summary provided by upstream source.

Repository SourceNeeds Review
General

angular-routing

No summary provided by upstream source.

Repository SourceNeeds Review