Claude Agent Skill · by Analogjs

Angular Signals

Angular Signals provides developers with a modern reactive state management system for Angular v20+ projects, offering synchronous, fine-grained reactivity thro

Install
Terminal · npx
$npx skills add https://github.com/analogjs/angular-skills --skill angular-signals
Works with Paperclip

How Angular Signals fits into a Paperclip company.

Angular Signals drops into any Paperclip agent that handles this kind of work. Assign it to a specialist inside a pre-configured PaperclipOrg company and the skill becomes available on every heartbeat — no prompt engineering, no tool wiring.

S
SaaS FactoryPaired

Pre-configured AI company — 18 agents, 18 skills, one-time purchase.

$27$59
Explore pack
Source file
SKILL.md302 lines
Expand
---name: angular-signalsdescription: 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 ```typescriptimport { signal } from '@angular/core'; // Create writable signalconst count = signal(0); // Read valueconsole.log(count()); // 0 // Set new valuecount.set(5); // Update based on current valuecount.update(c => c + 1); // With explicit typeconst user = signal<User | null>(null);user.set({ id: 1, name: 'Alice' });``` ### computed() - Derived State ```typescriptimport { signal, computed } from '@angular/core'; const firstName = signal('John');const lastName = signal('Doe'); // Derived signal - automatically updates when dependencies changeconst fullName = computed(() => `${firstName()} ${lastName()}`); console.log(fullName()); // "John Doe"firstName.set('Jane');console.log(fullName()); // "Jane Doe" // Computed with complex logicconst 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 ```typescriptimport { signal, linkedSignal } from '@angular/core'; const options = signal(['A', 'B', 'C']); // Resets to first option when options changeconst selected = linkedSignal(() => options()[0]); console.log(selected()); // "A"selected.set('B');       // User selects Bconsole.log(selected()); // "B"options.set(['X', 'Y']); // Options changeconsole.log(selected()); // "X" - auto-reset to first // With previous value accessconst 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 ```typescriptimport { 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 ```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 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 ```typescriptimport { 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 ```typescriptimport { 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 ```typescript// Custom equality functionconst user = signal<User>(  { id: 1, name: 'Alice' },  { equal: (a, b) => a.id === b.id }); // Only triggers updates when ID changesuser.set({ id: 1, name: 'Alice Updated' }); // No updateuser.set({ id: 2, name: 'Bob' }); // Triggers update``` ## Untracked Reads ```typescriptimport { 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 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](references/signal-patterns.md).