Skip to main content

angular-signals-patterns

Guides expert-level angular signals patterns implementation: typescript and frameworks decision frameworks, production-ready patterns, and concrete templates for angular signals patterns workflows. Use when the user asks about angular signals patterns, angular signals patterns configuration, or typescript best practices for angular projects. Do NOT use when the user needs a different web development capability -- check sibling skills in the web development subcategory.

Informations de source

Dépôt
FerroxLabs/murage
Dernière activité de la source
1 septembre 2026 à 13:26
Langue détectée de SKILL.md
anglais
Étoiles
9
Forks
2

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Explorateur de fichiers
2 fichiers

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
name
angular-signals-patterns
description
Guides expert-level angular signals patterns implementation: typescript and frameworks decision frameworks, production-ready patterns, and concrete templates for angular signals patterns workflows. Use when the user asks about angular signals patterns, angular signals patterns configuration, or typescript best practices for angular projects. Do NOT use when the user needs a different web development capability -- check sibling skills in the web development subcategory.
license
Apache-2.0
metadata
{"author":"foundry-skills","version":"1.0.0","tags":"typescript frameworks frontend architecture","category":"web-development","subcategory":"web-development","depends":"","disclaimer":"none","difficulty":"intermediate"}
# Angular Signals Patterns ## When to Use **Use this skill when:** - User asks how to implement Angular Signals (introduced in Angular 16, stabilized in Angular 17+) for reactive state management in components, services, or application-wide stores - User wants to replace RxJS-heavy component state with signal-based reactivity and needs guidance on which patterns to use and when - User asks about `signal()`, `computed()`, `effect()`, `toSignal()`, `toObservable()`, or the `input()` / `output()` / `model()` signal-based component APIs introduced in Angular 17--18 - User needs to design a signal-based state management architecture -- local component state, feature-level stores, or global application state using the SignalStore pattern from NgRx or custom implementations - User is migrating a component or service from `BehaviorSubject`-based patterns to signals and needs a concrete migration path with interop strategies - User wants to understand signal equality functions, custom comparators, or how to prevent unnecessary recomputations in deep object graphs - User asks about `linkedSignal()`, resource signals, or the experimental async signal primitives introduced in Angular 18+ - User is debugging unexpected signal re-evaluations, glitch-prone computed chains, or `effect()` infinite loops **Do NOT use this skill when:** - User is working with Angular versions below 16 -- signals do not exist; redirect to RxJS observable patterns for Angular state management - User needs general RxJS patterns not related to signal interop -- check the rxjs-patterns skill in this subcategory - User is building with React, Vue, Svelte, or Solid.js, even though those frameworks have analogous reactive primitives -- the APIs and scheduling semantics differ fundamentally - User needs Angular component architecture guidance unrelated to reactivity (routing, lazy loading, module organization) -- check the angular-architecture skill - User is asking about NgRx Actions/Reducers/Effects in the traditional Redux pattern -- check the ngrx-redux-patterns skill unless they specifically ask about NgRx SignalStore - User needs server-side rendering hydration strategies in Angular -- signals interact with SSR in specific ways covered by the angular-ssr skill --- ## Process ### 1. Identify the Reactivity Scope and State Ownership Before writing any signal code, classify what kind of state is being managed and where it lives. - **Local component state** -- data that only one component needs, never shared upward or across routes. Use `signal()` directly in the component class. No service injection needed. - **Shared feature state** -- data shared between sibling or parent/child components within one route or feature area. Use an injectable service with `signal()` properties, scoped with `providedIn: 'root'` or a feature-level `providers` array. - **Global application state** -- user session, auth tokens, app configuration, notifications. Use a dedicated signal-based store class, either hand-rolled or via NgRx SignalStore. - **Derived/computed state** -- anything that is a pure function of other signals. Always use `computed()` -- never store derived state in a writable signal and keep it synchronized with `effect()`. - Ask the user: "Is this state read by more than one component? Does it need to survive navigation? Does it need undo/redo or devtools inspection?" These answers determine the appropriate pattern tier. ### 2. Define the Signal Graph Structure Design the signal dependency graph before implementation to avoid creating brittle or cyclical dependencies. - Draw the full state as a directed acyclic graph (DAG): writable signals are leaf inputs, `computed()` signals are intermediate nodes, template bindings and `effect()` calls are terminal sinks. - Identify which signals are "source of truth" (`WritableSignal<T>`) vs. derived projections (`Signal<T>` from `computed()`). - Keep the graph shallow: chains longer than 3--4 levels of `computed()` calls become difficult to debug and can introduce latency in recomputation during a single change detection cycle. - Never make a computed signal depend on a `WritableSignal` that the computed signal's consumer also writes to -- this creates a logical cycle that Angular's glitch-free algorithm cannot resolve cleanly. - For collections, decide whether the signal holds the entire array (`signal<Item[]>([])`) or a map (`signal<Map<string, Item>>(new Map())`) based on how updates will be performed. Replacing the entire array on every mutation is correct but causes `computed()` consumers of individual items to recompute even when unrelated items change. ### 3. Implement Writable Signals with Correct Mutation Patterns Signal mutations must be performed correctly to trigger reactivity and avoid stale references. - Use `signal<T>(initialValue)` with an explicit generic type annotation always -- do not rely on inference when the initial value is `null`, `undefined`, or an empty array, as inference will produce too-narrow types. ```typescript // BAD: infers signal<never[]> const items = signal([]); // GOOD const items = signal<Item[]>([]); ``` - Use `.set(newValue)` when replacing the entire value. - Use `.update(prev => newValue)` when the new value depends on the previous value -- this is the correct pattern for array mutations and counter increments. ```typescript items.update(prev => [...prev, newItem]); // append items.update(prev => prev.filter(i => i.id !== id)); // remove count.update(n => n + 1); // increment ``` - Never mutate the signal's value in place (e.g., `items().push(x)`) -- this does not notify Angular's reactive graph and produces stale UI. - Use `.mutate()` only if available in your Angular version (it was removed in Angular 17.1 in favor of always-immutable updates with `.update()`). If the codebase is on 16.x, `.mutate()` exists but signals the team to plan for removal. - For deep objects, use a custom equality function to prevent re-renders when semantically identical objects are produced: ```typescript const config = signal<Config>(initialConfig, { equal: (a, b) => JSON.stringify(a) === JSON.stringify(b) }); ``` Use `JSON.stringify` only for small, serializable objects. For large objects, write a structural comparator or use a library like `fast-deep-equal`. ### 4. Build Computed Signals for Derived State `computed()` is the most powerful primitive for keeping derived state in sync without explicit subscription management. - Every `computed()` call creates a memoized, lazy value. It only recomputes when at least one of its signal dependencies has changed since the last read. - Express computed signals as pure functions -- no HTTP calls, no DOM manipulation, no logging inside `computed()`. Those belong in `effect()`. ```typescript readonly filteredItems = computed(() => this.items().filter(item => item.active && item.category === this.selectedCategory()) ); readonly totalPrice = computed(() => this.cartItems().reduce((sum, item) => sum + item.price * item.quantity, 0) ); ``` - Use `computed()` for template-facing boolean flags: `readonly isLoading = computed(() => this.status() === 'loading')`. This is cleaner than storing a separate `isLoading` writable signal. - Computed signals are not writable -- if you find yourself wanting to write to a computed value, you have identified a missing writable signal that the computed should depend on. - Computed signals are synchronous. They cannot await Promises or subscribe to Observables. For async derived values, use `toSignal()` wrapping an Observable pipeline, or the experimental `resource()` API in Angular 18+. - Avoid computeds that depend on more than 6--8 distinct signals -- this is a code smell indicating the component or service has too many responsibilities. Split the state. ### 5. Use `effect()` Correctly for Side Effects `effect()` is the most commonly misused signal API. Apply it with discipline. - `effect()` runs once immediately after creation and again whenever any signal it reads changes. It runs inside the Angular change detection context. - Legitimate uses for `effect()`: - Synchronizing signal state to `localStorage` or `sessionStorage` - Logging/analytics when specific signals change - Integrating with imperative third-party libraries (e.g., setting a chart library's data when a signal changes) - Triggering router navigation based on auth state changes - Illegitimate uses that indicate a design problem: - Writing to a signal inside `effect()` to keep two signals synchronized -- use `computed()` instead - Making HTTP calls inside `effect()` -- use an Observable or `resource()` API instead - Replacing `ngOnChanges` with `effect()` -- use signal-based `input()` with `computed()` instead - To write to a signal inside an `effect()` when truly necessary (rare), use the `allowSignalWrites` option: ```typescript effect(() => { const value = this.externalSignal(); this.localSignal.set(processValue(value)); }, { allowSignalWrites: true }); ``` This option exists to escape hatches, not as a default pattern. If you use it more than once per class, reconsider the design. - Always clean up `effect()` by using the `DestroyRef` injection or calling the returned cleanup function. In components and directives, effects registered in the constructor are cleaned up automatically on destroy. In services, use `inject(DestroyRef).onDestroy()` to clean up manually created effects. - Never create `effect()` calls outside of an injection context (constructor, field initializer, or a function called during injection) without explicitly providing an injector. ### 6. Wire Signal-Based Component Inputs and Outputs Angular 17.1+ introduced signal-based `input()`, `output()`, and `model()` APIs that replace `@Input()`, `@Output()`, and `@Input()/@Output()` pairs. - Use `input<T>()` for required inputs and `input<T>(defaultValue)` for optional inputs with defaults: ```typescript readonly userId = input.required<string>(); // throws if not provided readonly pageSize = input<number>(25); // optional, defaults to 25 ``` - Signal inputs are `InputSignal<T>`, which is a read-only `Signal<T>`. Use them in `computed()` and templates exactly like writable signals: `this.userId()`. - Use `model<T>()` for two-way binding -- it creates a `ModelSignal<T>` that is both readable and writable, generating an implicit `(valueChange)` event output: ```typescript readonly selectedDate = model<Date | null>(null); // Parent binds with [(selectedDate)]="parentDate" // Child updates with: this.selectedDate.set(newDate) ``` - Use `output<T>()` for event emissions -- it replaces `EventEmitter` and is not a signal but an `OutputEmitterRef`: ```typescript readonly itemSelected = output<Item>(); // Emit with: this.itemSelected.emit(item) ``` - Never mix `@Input()` decorators with `input()` signals in the same component -- it is confusing and produces maintenance problems. Migrate the entire component at once. - `input()` signals are not writable from inside the component -- only the parent can change them. If you need writable internal state that mirrors an input, derive it: `readonly internalValue = signal(this.externalInput())`. ### 7. Implement Signal-Based Service Stores For feature or global state, implement a signal store as a plain injectable service or using NgRx SignalStore. **Hand-rolled signal store pattern:** ```typescript @Injectable({ providedIn: 'root' }) export class CartStore { // Private writable signals -- only this store mutates state private readonly _items = signal<CartItem[]>([]); private readonly _status = signal<'idle' | 'loading' | 'error'>('idle'); // Public read-only projections readonly items: Signal<CartItem[]> = this._items.asReadonly(); readonly status: Signal<'idle' | 'loading' | 'error'> = this._status.asReadonly(); // Computed selectors readonly totalItems = computed(() => this._items().reduce((sum, i) => sum + i.quantity, 0)); readonly totalPrice = computed(() => this._items().reduce((sum, i) => sum + i.price * i.quantity, 0)); readonly isEmpty = computed(() => this._items().length === 0); // Commands (methods that mutate state) addItem(item: CartItem): void { this._items.update(items => { const existing = items.find(i => i.productId === item.productId); if (existing) { return items.map(i => i.productId === item.productId ? { ...i, quantity: i.quantity + item.quantity } : i ); } return [...items, item]; }); } removeItem(productId: string): void { this._items.update(items => items.filter(i => i.productId !== productId)); } clearCart(): void { this._items.set([]); } } ``` - Always expose state as `.asReadonly()` signals from services -- components should never directly mutate service-owned signals. - Group store methods into "commands" (state mutations) and "queries" (computed projections). Commands are methods; queries are `computed()` properties. - For async operations, integrate with `toSignal()` and manage loading/error state explicitly: ```typescript loadItems(): void { this._status.set('loading'); this.http.get<CartItem[]>('/api/cart').pipe( takeUntilDestroyed(this.destroyRef) ).subscribe({ next: items => { this._items.set(items); this._status.set('idle'); }, error: () => this._status.set('error') }); } ``` ### 8. Integrate Signals with RxJS Using Interop APIs Signals and Observables coexist in Angular -- know the correct bridge for each direction. - **Observable to Signal** -- use `toSignal(observable$, options)`: ```typescript readonly searchResults = toSignal( this.searchQuery$.pipe( debounceTime(300), distinctUntilChanged(), switchMap(query => this.searchService.search(query)) ), { initialValue: [] } // avoids Signal<T | undefined> ); ``` Always provide `initialValue` unless the observable is guaranteed to emit synchronously, because `toSignal()` without `initialValue` creates `Signal<T | undefined>` and causes undefined-access errors in templates. - **Signal to Observable** -- use `toObservable(signal)`: ```typescript readonly results$ = toObservable(this.searchQuery).pipe( debounceTime(300), switchMap(q => this.http.get<Result[]>(`/api/search?q=${q}`)) ); ``` `toObservable()` uses `effect()` internally and emits on each signal change. It must be called in an injection context. - For HTTP calls, prefer keeping the Observable pipeline and exposing the result as a signal via `toSignal()`. Do not convert HTTP Observables into Promises just to use with signals -- the Observable pipeline gives you operators like `switchMap`, `catchError`, and `retry` that are difficult to replicate imperatively. - The experimental `resource()` API in Angular 18+ handles the async signal use case natively: ```typescript readonly userResource = resource({ request: () => ({ id: this.userId() }), loader: ({ request }) => fetch(`/api/users/${request.id}`).then(r => r.json()) }); // Access: this.userResource.value(), this.userResource.isLoading(), this.userResource.error() ``` --- ## Output Format When advising on Angular Signals patterns, produce output in this structure: ``` ## Signal Architecture Assessment ### State Classification | State Slice | Type | Owner | Pattern | |-------------------|--------------|------------------|------------------|
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub