{
  "schema": "https://ai-atoms.com/schemas/skill-v1.json",
  "type": "skill",
  "id": "skill/angular-state-management",
  "version": "1.0.0",
  "name": "Angular State Management",
  "description": "Master 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.",
  "system_prompt_fragment": "# Angular State Management\n\nComprehensive guide to modern Angular state management patterns, from Signal-based local state to global stores and server state synchronization.\n\n## When to Use This Skill\n\n- Setting up global state management in Angular\n- Choosing between Signals, NgRx, or Akita\n- Managing component-level stores\n- Implementing optimistic updates\n- Debugging state-related issues\n- Migrating from legacy state patterns\n\n## Do Not Use This Skill When\n\n- The task is unrelated to Angular state management\n- You need React state management → use `react-state-management`\n\n---\n\n## Core Concepts\n\n### State Categories\n\n| Type             | Description                  | Solutions             |\n| ---------------- | ---------------------------- | --------------------- |\n| **Local State**  | Component-specific, UI state | Signals, `signal()`   |\n| **Shared State** | Between related components   | Signal services       |\n| **Global State** | App-wide, complex            | NgRx, Akita, Elf      |\n| **Server State** | Remote data, caching         | NgRx Query, RxAngular |\n| **URL State**    | Route parameters             | ActivatedRoute        |\n| **Form State**   | Input values, validation     | Reactive Forms        |\n\n### Selection Criteria\n\n```\nSmall app, simple state → Signal Services\nMedium app, moderate state → Component Stores\nLarge app, complex state → NgRx Store\nHeavy server interaction → NgRx Query + Signal Services\nReal-time updates → RxAngular + Signals\n```\n\n---\n\n## Quick Start: Signal-Based State\n\n### Pattern 1: Simple Signal Service\n\n```typescript\n// services/counter.service.ts\nimport { Injectable, signal, computed } from \"@angular/core\";\n\n@Injectable({ providedIn: \"root\" })\nexport class CounterService {\n  // Private writable signals\n  private _count = signal(0);\n\n  // Public read-only\n  readonly count = this._count.asReadonly();\n  readonly doubled = computed(() => this._count() * 2);\n  readonly isPositive = computed(() => this._count() > 0);\n\n  increment() {\n    this._count.update((v) => v + 1);\n  }\n\n  decrement() {\n    this._count.update((v) => v - 1);\n  }\n\n  reset() {\n    this._count.set(0);\n  }\n}\n\n// Usage in component\n@Component({\n  template: `\n    <p>Count: {{ counter.count() }}</p>\n    <p>Doubled: {{ counter.doubled() }}</p>\n    <button (click)=\"counter.increment()\">+</button>\n  `,\n})\nexport class CounterComponent {\n  counter = inject(CounterService);\n}\n```\n\n### Pattern 2: Feature Signal Store\n\n```typescript\n// stores/user.store.ts\nimport { Injectable, signal, computed, inject } from \"@angular/core\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { toSignal } from \"@angular/core/rxjs-interop\";\n\ninterface User {\n  id: string;\n  name: string;\n  email: string;\n}\n\ninterface UserState {\n  user: User | null;\n  loading: boolean;\n  error: string | null;\n}\n\n@Injectable({ providedIn: \"root\" })\nexport class UserStore {\n  private http = inject(HttpClient);\n\n  // State signals\n  private _user = signal<User | null>(null);\n  private _loading = signal(false);\n  private _error = signal<string | null>(null);\n\n  // Selectors (read-only computed)\n  readonly user = computed(() => this._user());\n  readonly loading = computed(() => this._loading());\n  readonly error = computed(() => this._error());\n  readonly isAuthenticated = computed(() => this._user() !== null);\n  readonly displayName = computed(() => this._user()?.name ?? \"Guest\");\n\n  // Actions\n  async loadUser(id: string) {\n    this._loading.set(true);\n    this._error.set(null);\n\n    try {\n      const user = await fetch(`/api/users/${id}`).then((r) => r.json());\n      this._user.set(user);\n    } catch (e) {\n      this._error.set(\"Failed to load user\");\n    } finally {\n      this._loading.set(false);\n    }\n  }\n\n  updateUser(updates: Partial<User>) {\n    this._user.update((user) => (user ? { ...user, ...updates } : null));\n  }\n\n  logout() {\n    this._user.set(null);\n    this._error.set(null);\n  }\n}\n```\n\n### Pattern 3: SignalStore (NgRx Signals)\n\n```typescript\n// stores/products.store.ts\nimport {\n  signalStore,\n  withState,\n  withMethods,\n  withComputed,\n  patchState,\n} from \"@ngrx/signals\";\nimport { inject } from \"@angular/core\";\nimport { ProductService } from \"./product.service\";\n\ninterface ProductState {\n  products: Product[];\n  loading: boolean;\n  filter: string;\n}\n\nconst initialState: ProductState = {\n  products: [],\n  loading: false,\n  filter: \"\",\n};\n\nexport const ProductStore = signalStore(\n  { providedIn: \"root\" },\n\n  withState(initialState),\n\n  withComputed((store) => ({\n    filteredProducts: computed(() => {\n      const filter = store.filter().toLowerCase();\n      return store\n        .products()\n        .filter((p) => p.name.toLowerCase().includes(filter));\n    }),\n    totalCount: computed(() => store.products().length),\n  })),\n\n  withMethods((store, productService = inject(ProductService)) => ({\n    async loadProducts() {\n      patchState(store, { loading: true });\n\n      try {\n        const products = await productService.getAll();\n        patchState(store, { products, loading: false });\n      } catch {\n        patchState(store, { loading: false });\n      }\n    },\n\n    setFilter(filter: string) {\n      patchState(store, { filter });\n    },\n\n    addProduct(product: Product) {\n      patchState(store, ({ products }) => ({\n        products: [...products, product],\n      }));\n    },\n  })),\n);\n\n// Usage\n@Component({\n  template: `\n    <input (input)=\"store.setFilter($event.target.value)\" />\n    @if (store.loading()) {\n      <app-spinner />\n    } @else {\n      @for (product of store.filteredProducts(); track product.id) {\n        <app-product-card [product]=\"product\" />\n      }\n    }\n  `,\n})\nexport class ProductListComponent {\n  store = inject(ProductStore);\n\n  ngOnInit() {\n    this.store.loadProducts();\n  }\n}\n```\n\n---\n\n## NgRx Store (Global State)\n\n### Setup\n\n```typescript\n// store/app.state.ts\nimport { ActionReducerMap } from \"@ngrx/store\";\n\nexport interface AppState {\n  user: UserState;\n  cart: CartState;\n}\n\nexport const reducers: ActionReducerMap<AppState> = {\n  user: userReducer,\n  cart: cartReducer,\n};\n\n// main.ts\nbootstrapApplication(AppComponent, {\n  providers: [\n    provideStore(reducers),\n    provideEffects([UserEffects, CartEffects]),\n    provideStoreDevtools({ maxAge: 25 }),\n  ],\n});\n```\n\n### Feature Slice Pattern\n\n```typescript\n// store/user/user.actions.ts\nimport { createActionGroup, props, emptyProps } from \"@ngrx/store\";\n\nexport const UserActions = createActionGroup({\n  source: \"User\",\n  events: {\n    \"Load User\": props<{ userId: string }>(),\n    \"Load User Success\": props<{ user: User }>(),\n    \"Load User Failure\": props<{ error: string }>(),\n    \"Update User\": props<{ updates: Partial<User> }>(),\n    Logout: emptyProps(),\n  },\n});\n```\n\n```typescript\n// store/user/user.reducer.ts\nimport { createReducer, on } from \"@ngrx/store\";\nimport { UserActions } from \"./user.actions\";\n\nexport interface UserState {\n  user: User | null;\n  loading: boolean;\n  error: string | null;\n}\n\nconst initialState: UserState = {\n  user: null,\n  loading: false,\n  error: null,\n};\n\nexport const userReducer = createReducer(\n  initialState,\n\n  on(UserActions.loadUser, (state) => ({\n    ...state,\n    loading: true,\n    error: null,\n  })),\n\n  on(UserActions.loadUserSuccess, (state, { user }) => ({\n    ...state,\n    user,\n    loading: false,\n  })),\n\n  on(UserActions.loadUserFailure, (state, { error }) => ({\n    ...state,\n    loading: false,\n    error,\n  })),\n\n  on(UserActions.logout, () => initialState),\n);\n```\n\n```typescript\n// store/user/user.selectors.ts\nimport { createFeatureSelector, createSelector } from \"@ngrx/store\";\nimport { UserState } from \"./user.reducer\";\n\nexport const selectUserState = createFeatureSelector<UserState>(\"user\");\n\nexport const selectUser = createSelector(\n  selectUserState,\n  (state) => state.user,\n);\n\nexport const selectUserLoading = createSelector(\n  selectUserState,\n  (state) => state.loading,\n);\n\nexport const selectIsAuthenticated = createSelector(\n  selectUser,\n  (user) => user !== null,\n);\n```\n\n```typescript\n// store/user/user.effects.ts\nimport { Injectable, inject } from \"@angular/core\";\nimport { Actions, createEffect, ofType } from \"@ngrx/effects\";\nimport { switchMap, map, catchError, of } from \"rxjs\";\n\n@Injectable()\nexport class UserEffects {\n  private actions$ = inject(Actions);\n  private userService = inject(UserService);\n\n  loadUser$ = createEffect(() =>\n    this.actions$.pipe(\n      ofType(UserActions.loadUser),\n      switchMap(({ userId }) =>\n        this.userService.getUser(userId).pipe(\n          map((user) => UserActions.loadUserSuccess({ user })),\n          catchError((error) =>\n            of(UserActions.loadUserFailure({ error: error.message })),\n          ),\n        ),\n      ),\n    ),\n  );\n}\n```\n\n### Component Usage\n\n```typescript\n@Component({\n  template: `\n    @if (loading()) {\n      <app-spinner />\n    } @else if (user(); as user) {\n      <h1>Welcome, {{ user.name }}</h1>\n      <button (click)=\"logout()\">Logout</button>\n    }\n  `,\n})\nexport class HeaderComponent {\n  private store = inject(Store);\n\n  user = this.store.selectSignal(selectUser);\n  loading = this.store.selectSignal(selectUserLoading);\n\n  logout() {\n    this.store.dispatch(UserActions.logout());\n  }\n}\n```\n\n---\n\n## RxJS-Based Patterns\n\n### Component Store (Local Feature State)\n\n```typescript\n// stores/todo.store.ts\nimport { Injectable } from \"@angular/core\";\nimport { ComponentStore } from \"@ngrx/component-store\";\nimport { switchMap, tap, catchError, EMPTY } from \"rxjs\";\n\ninterface TodoState {\n  todos: Todo[];\n  loading: boolean;\n}\n\n@Injectable()\nexport class TodoStore extends ComponentStore<TodoState> {\n  constructor(private todoService: TodoService) {\n    super({ todos: [], loading: false });\n  }\n\n  // Selectors\n  readonly todos$ = this.select((state) => state.todos);\n  readonly loading$ = this.select((state) => state.loading);\n  readonly completedCount$ = this.select(\n    this.todos$,\n    (todos) => todos.filter((t) => t.completed).length,\n  );\n\n  // Updaters\n  readonly addTodo = this.updater((state, todo: Todo) => ({\n    ...state,\n    todos: [...state.todos, todo],\n  }));\n\n  readonly toggleTodo = this.updater((state, id: string) => ({\n    ...state,\n    todos: state.todos.map((t) =>\n      t.id === id ? { ...t, completed: !t.completed } : t,\n    ),\n  }));\n\n  // Effects\n  readonly loadTodos = this.effect<void>((trigger$) =>\n    trigger$.pipe(\n      tap(() => this.patchState({ loading: true })),\n      switchMap(() =>\n        this.todoService.getAll().pipe(\n          tap({\n            next: (todos) => this.patchState({ todos, loading: false }),\n            error: () => this.patchState({ loading: false }),\n          }),\n          catchError(() => EMPTY),\n        ),\n      ),\n    ),\n  );\n}\n```\n\n---\n\n## Server State with Signals\n\n### HTTP + Signals Pattern\n\n```typescript\n// services/api.service.ts\nimport { Injectable, signal, inject } from \"@angular/core\";\nimport { HttpClient } from \"@angular/common/http\";\nimport { toSignal } from \"@angular/core/rxjs-interop\";\n\ninterface ApiState<T> {\n  data: T | null;\n  loading: boolean;\n  error: string | null;\n}\n\n@Injectable({ providedIn: \"root\" })\nexport class ProductApiService {\n  private http = inject(HttpClient);\n\n  private _state = signal<ApiState<Product[]>>({\n    data: null,\n    loading: false,\n    error: null,\n  });\n\n  readonly products = computed(() => this._state().data ?? []);\n  readonly loading = computed(() => this._state().loading);\n  readonly error = computed(() => this._state().error);\n\n  async fetchProducts(): Promise<void> {\n    this._state.update((s) => ({ ...s, loading: true, error: null }));\n\n    try {\n      const data = await firstValueFrom(\n        this.http.get<Product[]>(\"/api/products\"),\n      );\n      this._state.update((s) => ({ ...s, data, loading: false }));\n    } catch (e) {\n      this._state.update((s) => ({\n        ...s,\n        loading: false,\n        error: \"Failed to fetch products\",\n      }));\n    }\n  }\n\n  // Optimistic update\n  async deleteProduct(id: string): Promise<void> {\n    const previousData = this._state().data;\n\n    // Optimistically remove\n    this._state.update((s) => ({\n      ...s,\n      data: s.data?.filter((p) => p.id !== id) ?? null,\n    }));\n\n    try {\n      await firstValueFrom(this.http.delete(`/api/products/${id}`));\n    } catch {\n      // Rollback on error\n      this._state.update((s) => ({ ...s, data: previousData }));\n    }\n  }\n}\n```\n\n---\n\n## Best Practices\n\n### Do's\n\n| Practice                           | Why                                |\n| ---------------------------------- | ---------------------------------- |\n| Use Signals for local state        | Simple, reactive, no subscriptions |\n| Use `computed()` for derived data  | Auto-updates, memoized             |\n| Colocate state with feature        | Easier to maintain                 |\n| Use NgRx for complex flows         | Actions, effects, devtools         |\n| Prefer `inject()` over constructor | Cleaner, works in factories        |\n\n### Don'ts\n\n| Anti-Pattern                      | Instead                                               |\n| --------------------------------- | ----------------------------------------------------- |\n| Store derived data                | Use `computed()`                                      |\n| Mutate signals directly           | Use `set()` or `update()`                             |\n| Over-globalize state              | Keep local when possible                              |\n| Mix RxJS and Signals chaotically  | Choose primary, bridge with `toSignal`/`toObservable` |\n| Subscribe in components for state | Use template with signals                             |\n\n---\n\n## Migration Path\n\n### From BehaviorSubject to Signals\n\n```typescript\n// Before: RxJS-based\n@Injectable({ providedIn: \"root\" })\nexport class OldUserService {\n  private userSubject = new BehaviorSubject<User | null>(null);\n  user$ = this.userSubject.asObservable();\n\n  setUser(user: User) {\n    this.userSubject.next(user);\n  }\n}\n\n// After: Signal-based\n@Injectable({ providedIn: \"root\" })\nexport class UserService {\n  private _user = signal<User | null>(null);\n  readonly user = this._user.asReadonly();\n\n  setUser(user: User) {\n    this._user.set(user);\n  }\n}\n```\n\n### Bridging Signals and RxJS\n\n```typescript\nimport { toSignal, toObservable } from '@angular/core/rxjs-interop';\n\n// Observable → Signal\n@Component({...})\nexport class ExampleComponent {\n  private route = inject(ActivatedRoute);\n\n  // Convert Observable to Signal\n  userId = toSignal(\n    this.route.params.pipe(map(p => p['id'])),\n    { initialValue: '' }\n  );\n}\n\n// Signal → Observable\nexport class DataService {\n  private filter = signal('');\n\n  // Convert Signal to Observable\n  filter$ = toObservable(this.filter);\n\n  filteredData$ = this.filter$.pipe(\n    debounceTime(300),\n    switchMap(filter => this.http.get(`/api/data?q=${filter}`))\n  );\n}\n```\n\n---\n\n## Resources\n\n- [Angular Signals Guide](https://angular.dev/guide/signals)\n- [NgRx Documentation](https://ngrx.io/)\n- [NgRx SignalStore](https://ngrx.io/guide/signals)\n- [RxAngular](https://www.rx-angular.io/)",
  "applicable_domains": [
    "frontend"
  ],
  "category": "frontend",
  "invocation": [
    "/angular-state-management"
  ],
  "authored_by": "claudeskills.in community",
  "source_url": "https://claudeskills.in/skill/angular-state-management",
  "provenance": {
    "source": "claudeskills.in",
    "source_url": "https://claudeskills.in/skill/angular-state-management",
    "license": "unknown",
    "imported_at": "2026-09-03",
    "notes": "Aggregated by claudeskills.in from community GitHub lists."
  },
  "tags": [
    "claudeskills",
    "frontend",
    "risk-reviewed"
  ],
  "lifecycle": "draft"
}