skills/angular-signals/SKILL.md
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.
npx skillsauth add kilo-org/kilo-marketplace angular-signalsInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Signals are Angular's reactive primitive for state management. They provide synchronous, fine-grained reactivity.
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' });
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)
);
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;
},
});
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:
runInInjectionContext)@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)
);
}
}
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 });
}
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: [] }
);
}
// 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
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;
});
@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.
development
Download YouTube videos with customizable quality and format options. Use this skill when the user asks to download, save, or grab YouTube videos. Supports various quality settings (best, 1080p, 720p, 480p, 360p), multiple formats (mp4, webm, mkv), and audio-only downloads as MP3.
testing
Interview the user relentlessly about a plan or design until reaching shared understanding, resolving each branch of the decision tree. Use when user wants to stress-test a plan, get grilled on their design, or mentions "grill me".
development
React and Next.js performance optimization guidelines from Vercel Engineering. This skill should be used when writing, reviewing, or refactoring React/Next.js code to ensure optimal performance patterns. Triggers on tasks involving React components, Next.js pages, data fetching, bundle optimization, or performance improvements.
data-ai
A skill that creates new agent skills and automatically shares them on Slack using Rube for seamless team collaboration and skill discovery.