skills/state-management-angular/SKILL.md
Manage frontend state in Angular 19 applications. Covers Angular signals, signal store (NgRx SignalStore), RxJS-based service state, URL state with Router, and state composition patterns. Use when: choosing a state management approach, implementing caching, managing global UI state, or optimizing change detection.
npx skillsauth add congiuluc/my-awesome-copilot state-management-angularInstall 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.
signal() and computed() for reactive state.computed() instead of storing derived values.| Category | Tool | Examples |
|----------|------|---------|
| Local UI | signal(), computed() | Form inputs, open/close, selection |
| Feature | NgRx SignalStore | Feature-level CRUD state, complex workflows |
| Global UI | Service + signals | Theme, auth status, sidebar |
| URL | ActivatedRoute, Router | Filters, sort, page number |
| Form | Typed Reactive Forms | Validation, submission, field state |
import { Component, signal, computed, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'app-counter',
standalone: true,
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<p>Count: {{ count() }}</p>
<p>Double: {{ doubleCount() }}</p>
<button (click)="increment()">+</button>
`,
})
export class CounterComponent {
readonly count = signal(0);
readonly doubleCount = computed(() => this.count() * 2);
increment() {
this.count.update((c) => c + 1);
}
}
signal() for mutable state.computed() for derived state.update() for state transitions based on previous value.import { signalStore, withState, withMethods, withComputed, patchState } from '@ngrx/signals';
import { inject } from '@angular/core';
import { ProductService } from '../services/product.service';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { pipe, switchMap, tap } from 'rxjs';
import { tapResponse } from '@ngrx/operators';
interface ProductState {
products: Product[];
loading: boolean;
error: string | null;
selectedId: string | null;
}
const initialState: ProductState = {
products: [],
loading: false,
error: null,
selectedId: null,
};
export const ProductStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withComputed((store) => ({
selectedProduct: computed(() =>
store.products().find((p) => p.id === store.selectedId())
),
productCount: computed(() => store.products().length),
})),
withMethods((store, productService = inject(ProductService)) => ({
loadProducts: rxMethod<void>(
pipe(
tap(() => patchState(store, { loading: true, error: null })),
switchMap(() =>
productService.getAll().pipe(
tapResponse({
next: (products) => patchState(store, { products, loading: false }),
error: (error: Error) =>
patchState(store, { error: error.message, loading: false }),
})
)
)
)
),
selectProduct(id: string) {
patchState(store, { selectedId: id });
},
}))
);
signalStore for feature-level state that multiple components share.withState for initial state shape.withComputed for derived state.withMethods + rxMethod for async operations.patchState for immutable updates.import { Injectable, signal, computed } from '@angular/core';
@Injectable({ providedIn: 'root' })
export class ThemeService {
private readonly themeSignal = signal<'light' | 'dark'>('dark');
readonly theme = this.themeSignal.asReadonly();
readonly isDark = computed(() => this.themeSignal() === 'dark');
toggle() {
this.themeSignal.update((t) => (t === 'light' ? 'dark' : 'light'));
}
}
signal() and public asReadonly().computed() for derived state.import { Component, inject } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router';
import { toSignal } from '@angular/core/rxjs-interop';
import { map } from 'rxjs';
@Component({ /* ... */ })
export class ProductListComponent {
private readonly route = inject(ActivatedRoute);
private readonly router = inject(Router);
readonly page = toSignal(
this.route.queryParamMap.pipe(
map((params) => Number(params.get('page') ?? '1'))
),
{ initialValue: 1 }
);
goToPage(page: number) {
this.router.navigate([], {
queryParams: { page },
queryParamsHandling: 'merge',
});
}
}
toSignal() to bridge RxJS route params into signals.queryParamsHandling: 'merge' to preserve other params.import { toSignal, toObservable } from '@angular/core/rxjs-interop';
// Observable → Signal
const data = toSignal(observable$, { initialValue: [] });
// Signal → Observable
const data$ = toObservable(signalValue);
toSignal() when consuming observables in templates.toObservable() when feeding signals into RxJS pipelines.initialValue for toSignal() to avoid undefined.signal() insteadcomputed() insteadtoSignal() or async pipeset(), update(), or patchState()tools
Build VS Code extensions with TypeScript. Covers extension anatomy, activation events, commands, tree views, webview panels, language features, testing, and publishing. Use when: creating a new VS Code extension, adding commands/views/providers, building webview UIs, implementing language server features, testing extensions, or packaging for the marketplace.
development
Track implementations, features, bugs, and releases in a versioning document. Use when: adding a commit, completing a feature, fixing a bug, or preparing a release. Automatically updates CHANGELOG.md following Keep a Changelog format and Semantic Versioning.
development
Write frontend tests using Vitest and React Testing Library. Use when: testing React components, hooks, user interactions, form submissions, accessibility assertions, or mocking API services.
development
Write Angular frontend tests using Jasmine, Karma, and Angular TestBed. Use when: testing Angular components, services, pipes, directives, user interactions, form submissions, accessibility assertions, or mocking HTTP services.