$npx -y skills add SamarthaKV29/antigravity-god-mode --skill angular-state-managementMaster modern Angular state management with Signals, NgRx, and RxJS. Use when setting up global state, managing component stores, choosing between state solutions, or migrating from legacy patterns.
| 1 | # Angular State Management |
| 2 | |
| 3 | Comprehensive guide to modern Angular state management patterns, from Signal-based local state to global stores and server state synchronization. |
| 4 | |
| 5 | ## When to Use This Skill |
| 6 | |
| 7 | - Setting up global state management in Angular |
| 8 | - Choosing between Signals, NgRx, or Akita |
| 9 | - Managing component-level stores |
| 10 | - Implementing optimistic updates |
| 11 | - Debugging state-related issues |
| 12 | - Migrating from legacy state patterns |
| 13 | |
| 14 | ## Do Not Use This Skill When |
| 15 | |
| 16 | - The task is unrelated to Angular state management |
| 17 | - You need React state management → use `react-state-management` |
| 18 | |
| 19 | --- |
| 20 | |
| 21 | ## Core Concepts |
| 22 | |
| 23 | ### State Categories |
| 24 | |
| 25 | | Type | Description | Solutions | |
| 26 | | ---------------- | ---------------------------- | --------------------- | |
| 27 | | **Local State** | Component-specific, UI state | Signals, `signal()` | |
| 28 | | **Shared State** | Between related components | Signal services | |
| 29 | | **Global State** | App-wide, complex | NgRx, Akita, Elf | |
| 30 | | **Server State** | Remote data, caching | NgRx Query, RxAngular | |
| 31 | | **URL State** | Route parameters | ActivatedRoute | |
| 32 | | **Form State** | Input values, validation | Reactive Forms | |
| 33 | |
| 34 | ### Selection Criteria |
| 35 | |
| 36 | ``` |
| 37 | Small app, simple state → Signal Services |
| 38 | Medium app, moderate state → Component Stores |
| 39 | Large app, complex state → NgRx Store |
| 40 | Heavy server interaction → NgRx Query + Signal Services |
| 41 | Real-time updates → RxAngular + Signals |
| 42 | ``` |
| 43 | |
| 44 | --- |
| 45 | |
| 46 | ## Quick Start: Signal-Based State |
| 47 | |
| 48 | ### Pattern 1: Simple Signal Service |
| 49 | |
| 50 | ```typescript |
| 51 | // services/counter.service.ts |
| 52 | import { Injectable, signal, computed } from "@angular/core"; |
| 53 | |
| 54 | @Injectable({ providedIn: "root" }) |
| 55 | export class CounterService { |
| 56 | // Private writable signals |
| 57 | private _count = signal(0); |
| 58 | |
| 59 | // Public read-only |
| 60 | readonly count = this._count.asReadonly(); |
| 61 | readonly doubled = computed(() => this._count() * 2); |
| 62 | readonly isPositive = computed(() => this._count() > 0); |
| 63 | |
| 64 | increment() { |
| 65 | this._count.update((v) => v + 1); |
| 66 | } |
| 67 | |
| 68 | decrement() { |
| 69 | this._count.update((v) => v - 1); |
| 70 | } |
| 71 | |
| 72 | reset() { |
| 73 | this._count.set(0); |
| 74 | } |
| 75 | } |
| 76 | |
| 77 | // Usage in component |
| 78 | @Component({ |
| 79 | template: ` |
| 80 | <p>Count: {{ counter.count() }}</p> |
| 81 | <p>Doubled: {{ counter.doubled() }}</p> |
| 82 | <button (click)="counter.increment()">+</button> |
| 83 | `, |
| 84 | }) |
| 85 | export class CounterComponent { |
| 86 | counter = inject(CounterService); |
| 87 | } |
| 88 | ``` |
| 89 | |
| 90 | ### Pattern 2: Feature Signal Store |
| 91 | |
| 92 | ```typescript |
| 93 | // stores/user.store.ts |
| 94 | import { Injectable, signal, computed, inject } from "@angular/core"; |
| 95 | import { HttpClient } from "@angular/common/http"; |
| 96 | import { toSignal } from "@angular/core/rxjs-interop"; |
| 97 | |
| 98 | interface User { |
| 99 | id: string; |
| 100 | name: string; |
| 101 | email: string; |
| 102 | } |
| 103 | |
| 104 | interface UserState { |
| 105 | user: User | null; |
| 106 | loading: boolean; |
| 107 | error: string | null; |
| 108 | } |
| 109 | |
| 110 | @Injectable({ providedIn: "root" }) |
| 111 | export class UserStore { |
| 112 | private http = inject(HttpClient); |
| 113 | |
| 114 | // State signals |
| 115 | private _user = signal<User | null>(null); |
| 116 | private _loading = signal(false); |
| 117 | private _error = signal<string | null>(null); |
| 118 | |
| 119 | // Selectors (read-only computed) |
| 120 | readonly user = computed(() => this._user()); |
| 121 | readonly loading = computed(() => this._loading()); |
| 122 | readonly error = computed(() => this._error()); |
| 123 | readonly isAuthenticated = computed(() => this._user() !== null); |
| 124 | readonly displayName = computed(() => this._user()?.name ?? "Guest"); |
| 125 | |
| 126 | // Actions |
| 127 | async loadUser(id: string) { |
| 128 | this._loading.set(true); |
| 129 | this._error.set(null); |
| 130 | |
| 131 | try { |
| 132 | const user = await fetch(`/api/users/${id}`).then((r) => r.json()); |
| 133 | this._user.set(user); |
| 134 | } catch (e) { |
| 135 | this._error.set("Failed to load user"); |
| 136 | } finally { |
| 137 | this._loading.set(false); |
| 138 | } |
| 139 | } |
| 140 | |
| 141 | updateUser(updates: Partial<User>) { |
| 142 | this._user.update((user) => (user ? { ...user, ...updates } : null)); |
| 143 | } |
| 144 | |
| 145 | logout() { |
| 146 | this._user.set(null); |
| 147 | this._error.set(null); |
| 148 | } |
| 149 | } |
| 150 | ``` |
| 151 | |
| 152 | ### Pattern 3: SignalStore (NgRx Signals) |
| 153 | |
| 154 | ```typescript |
| 155 | // stores/products.store.ts |
| 156 | import { |
| 157 | signalStore, |
| 158 | withState, |
| 159 | withMethods, |
| 160 | withComputed, |
| 161 | patchState, |
| 162 | } from "@ngrx/signals"; |
| 163 | import { inject } from "@angular/core"; |
| 164 | import { ProductService } from "./product.service"; |
| 165 | |
| 166 | interface ProductState { |
| 167 | products: Product[]; |
| 168 | loading: boolean; |
| 169 | filter: string; |
| 170 | } |
| 171 | |
| 172 | const initialState: ProductState = { |
| 173 | products: [], |
| 174 | loading: false, |
| 175 | filter: "", |
| 176 | }; |
| 177 | |
| 178 | export const ProductStore = signalStore( |
| 179 | { providedIn: "root" }, |
| 180 | |
| 181 | withState(initialState), |
| 182 | |
| 183 | withComputed((store) => ({ |
| 184 | filteredProducts: computed(() => { |
| 185 | const filter = store.filter().toLowerCase(); |
| 186 | return store |
| 187 | .products() |
| 188 | .filter((p) => p.name.toLowerCase().includes(filter)); |
| 189 | }), |