Smithery Logo
MCPsSkillsDocsPricing
Login
Smithery Logo

Accelerating the Agent Economy

Resources

DocumentationPrivacy PolicySystem Status

Company

PricingAboutBlog

Connect

© 2026 Smithery. All rights reserved.

    neversight

    angular-signals

    neversight/angular-signals
    Coding
    2
    1 installs

    About

    SKILL.md

    Install

    Install via Skills CLI

    or add to your agent
    • Claude Code
      Claude Code
    • Codex
      Codex
    • OpenClaw
      OpenClaw
    • Cursor
      Cursor
    • Amp
      Amp
    • GitHub Copilot
      GitHub Copilot
    • Gemini CLI
      Gemini CLI
    • Kilo Code
      Kilo Code
    • Junie
      Junie
    • Replit
      Replit
    • Windsurf
      Windsurf
    • Cline
      Cline
    • Continue
      Continue
    • OpenCode
      OpenCode
    • OpenHands
      OpenHands
    • Roo Code
      Roo Code
    • Augment
      Augment
    • Goose
      Goose
    • Trae
      Trae
    • Zencoder
      Zencoder
    • Antigravity
      Antigravity
    ├─
    ├─
    └─

    About

    Implement signal-based reactive state management in Angular v20+.

    SKILL.md

    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 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
    // Writing signals in effects (use sparingly)
    effect(() => {
      if (this.query().length > 0) {
        this.hasSearched.set(true);
      }
    }, { allowSignalWrites: true });
    

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

    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

    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

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

    Repository
    neversight/skills_feed