[{"data":1,"prerenderedAt":-1},["ShallowReactive",2],{"skill:01M7155W46ASQN2ZE111AV1CZM":3,"skill-same:01M7155W46ASQN2ZE111AV1CZM":75},{"skillNo":4,"name":5,"nameZh":6,"description":7,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":16,"categories":17,"keywords":18,"publishedAt":39,"linkOnly":40,"markdownContent":41,"markdownBody":42,"frontMatter":43,"refName":45,"commitSha":46,"skillDirectory":47,"forksCount":14,"repositoryHtmlUrl":48,"repositoryHomepageUrl":6,"ownerHtmlUrl":49,"ownerBlogUrl":6,"ownerDescription":6,"updatedAt":50,"install":51,"resources":64},"01M7155W46ASQN2ZE111AV1CZM","angular-state-management",null,"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.","sapsapshen\u002Fai-memory-router","sapsapshen","https:\u002F\u002Favatars.githubusercontent.com\u002Fu\u002F20347959?v=4","01M6XDQEF1DP50DKF37H81J3J5","shenmian","01M6XDQEJPE41Y3DT2763X8ZJN",0,"MIT","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcursor\u002Fangular-state-management\u002FSKILL.md",[],[19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38],"state","state management","angular state management","management","angular","global","patterns","master","safe","2026","signals","component","solutions","self","with","when","Legacy","use","ngrx","up","2026-11-10 10:12:30",false,"---\nname: angular-state-management\ndescription: \"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.\"\nrisk: safe\nsource: self\ndate_added: \"2026-02-27\"\n---\n\n# 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\u002F\u002F services\u002Fcounter.service.ts\nimport { Injectable, signal, computed } from \"@angular\u002Fcore\";\n\n@Injectable({ providedIn: \"root\" })\nexport class CounterService {\n  \u002F\u002F Private writable signals\n  private _count = signal(0);\n\n  \u002F\u002F 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\u002F\u002F Usage in component\n@Component({\n  template: `\n    \u003Cp>Count: {{ counter.count() }}\u003C\u002Fp>\n    \u003Cp>Doubled: {{ counter.doubled() }}\u003C\u002Fp>\n    \u003Cbutton (click)=\"counter.increment()\">+\u003C\u002Fbutton>\n  `,\n})\nexport class CounterComponent {\n  counter = inject(CounterService);\n}\n```\n\n### Pattern 2: Feature Signal Store\n\n```typescript\n\u002F\u002F stores\u002Fuser.store.ts\nimport { Injectable, signal, computed, inject } from \"@angular\u002Fcore\";\nimport { HttpClient } from \"@angular\u002Fcommon\u002Fhttp\";\nimport { toSignal } from \"@angular\u002Fcore\u002Frxjs-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  \u002F\u002F State signals\n  private _user = signal\u003CUser | null>(null);\n  private _loading = signal(false);\n  private _error = signal\u003Cstring | null>(null);\n\n  \u002F\u002F 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  \u002F\u002F Actions\n  async loadUser(id: string) {\n    this._loading.set(true);\n    this._error.set(null);\n\n    try {\n      const user = await fetch(`\u002Fapi\u002Fusers\u002F${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\u003CUser>) {\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\u002F\u002F stores\u002Fproducts.store.ts\nimport {\n  signalStore,\n  withState,\n  withMethods,\n  withComputed,\n  patchState,\n} from \"@ngrx\u002Fsignals\";\nimport { inject } from \"@angular\u002Fcore\";\nimport { ProductService } from \".\u002Fproduct.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\u002F\u002F Usage\n@Component({\n  template: `\n    \u003Cinput (input)=\"store.setFilter($event.target.value)\" \u002F>\n    @if (store.loading()) {\n      \u003Capp-spinner \u002F>\n    } @else {\n      @for (product of store.filteredProducts(); track product.id) {\n        \u003Capp-product-card [product]=\"product\" \u002F>\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\u002F\u002F store\u002Fapp.state.ts\nimport { ActionReducerMap } from \"@ngrx\u002Fstore\";\n\nexport interface AppState {\n  user: UserState;\n  cart: CartState;\n}\n\nexport const reducers: ActionReducerMap\u003CAppState> = {\n  user: userReducer,\n  cart: cartReducer,\n};\n\n\u002F\u002F 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\u002F\u002F store\u002Fuser\u002Fuser.actions.ts\nimport { createActionGroup, props, emptyProps } from \"@ngrx\u002Fstore\";\n\nexport const UserActions = createActionGroup({\n  source: \"User\",\n  events: {\n    \"Load User\": props\u003C{ userId: string }>(),\n    \"Load User Success\": props\u003C{ user: User }>(),\n    \"Load User Failure\": props\u003C{ error: string }>(),\n    \"Update User\": props\u003C{ updates: Partial\u003CUser> }>(),\n    Logout: emptyProps(),\n  },\n});\n```\n\n```typescript\n\u002F\u002F store\u002Fuser\u002Fuser.reducer.ts\nimport { createReducer, on } from \"@ngrx\u002Fstore\";\nimport { UserActions } from \".\u002Fuser.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\u002F\u002F store\u002Fuser\u002Fuser.selectors.ts\nimport { createFeatureSelector, createSelector } from \"@ngrx\u002Fstore\";\nimport { UserState } from \".\u002Fuser.reducer\";\n\nexport const selectUserState = createFeatureSelector\u003CUserState>(\"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\u002F\u002F store\u002Fuser\u002Fuser.effects.ts\nimport { Injectable, inject } from \"@angular\u002Fcore\";\nimport { Actions, createEffect, ofType } from \"@ngrx\u002Feffects\";\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      \u003Capp-spinner \u002F>\n    } @else if (user(); as user) {\n      \u003Ch1>Welcome, {{ user.name }}\u003C\u002Fh1>\n      \u003Cbutton (click)=\"logout()\">Logout\u003C\u002Fbutton>\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\u002F\u002F stores\u002Ftodo.store.ts\nimport { Injectable } from \"@angular\u002Fcore\";\nimport { ComponentStore } from \"@ngrx\u002Fcomponent-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\u003CTodoState> {\n  constructor(private todoService: TodoService) {\n    super({ todos: [], loading: false });\n  }\n\n  \u002F\u002F 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  \u002F\u002F 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  \u002F\u002F Effects\n  readonly loadTodos = this.effect\u003Cvoid>((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\u002F\u002F services\u002Fapi.service.ts\nimport { Injectable, signal, inject } from \"@angular\u002Fcore\";\nimport { HttpClient } from \"@angular\u002Fcommon\u002Fhttp\";\nimport { toSignal } from \"@angular\u002Fcore\u002Frxjs-interop\";\n\ninterface ApiState\u003CT> {\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\u003CApiState\u003CProduct[]>>({\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\u003Cvoid> {\n    this._state.update((s) => ({ ...s, loading: true, error: null }));\n\n    try {\n      const data = await firstValueFrom(\n        this.http.get\u003CProduct[]>(\"\u002Fapi\u002Fproducts\"),\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  \u002F\u002F Optimistic update\n  async deleteProduct(id: string): Promise\u003Cvoid> {\n    const previousData = this._state().data;\n\n    \u002F\u002F 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(`\u002Fapi\u002Fproducts\u002F${id}`));\n    } catch {\n      \u002F\u002F 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`\u002F`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\u002F\u002F Before: RxJS-based\n@Injectable({ providedIn: \"root\" })\nexport class OldUserService {\n  private userSubject = new BehaviorSubject\u003CUser | null>(null);\n  user$ = this.userSubject.asObservable();\n\n  setUser(user: User) {\n    this.userSubject.next(user);\n  }\n}\n\n\u002F\u002F After: Signal-based\n@Injectable({ providedIn: \"root\" })\nexport class UserService {\n  private _user = signal\u003CUser | 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\u002Fcore\u002Frxjs-interop';\n\n\u002F\u002F Observable → Signal\n@Component({...})\nexport class ExampleComponent {\n  private route = inject(ActivatedRoute);\n\n  \u002F\u002F Convert Observable to Signal\n  userId = toSignal(\n    this.route.params.pipe(map(p => p['id'])),\n    { initialValue: '' }\n  );\n}\n\n\u002F\u002F Signal → Observable\nexport class DataService {\n  private filter = signal('');\n\n  \u002F\u002F Convert Signal to Observable\n  filter$ = toObservable(this.filter);\n\n  filteredData$ = this.filter$.pipe(\n    debounceTime(300),\n    switchMap(filter => this.http.get(`\u002Fapi\u002Fdata?q=${filter}`))\n  );\n}\n```\n\n---\n\n## Resources\n\n- [Angular Signals Guide](https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals)\n- [NgRx Documentation](https:\u002F\u002Fngrx.io\u002F)\n- [NgRx SignalStore](https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals)\n- [RxAngular](https:\u002F\u002Fwww.rx-angular.io\u002F)\n","# 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\u002F\u002F services\u002Fcounter.service.ts\nimport { Injectable, signal, computed } from \"@angular\u002Fcore\";\n\n@Injectable({ providedIn: \"root\" })\nexport class CounterService {\n  \u002F\u002F Private writable signals\n  private _count = signal(0);\n\n  \u002F\u002F 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\u002F\u002F Usage in component\n@Component({\n  template: `\n    \u003Cp>Count: {{ counter.count() }}\u003C\u002Fp>\n    \u003Cp>Doubled: {{ counter.doubled() }}\u003C\u002Fp>\n    \u003Cbutton (click)=\"counter.increment()\">+\u003C\u002Fbutton>\n  `,\n})\nexport class CounterComponent {\n  counter = inject(CounterService);\n}\n```\n\n### Pattern 2: Feature Signal Store\n\n```typescript\n\u002F\u002F stores\u002Fuser.store.ts\nimport { Injectable, signal, computed, inject } from \"@angular\u002Fcore\";\nimport { HttpClient } from \"@angular\u002Fcommon\u002Fhttp\";\nimport { toSignal } from \"@angular\u002Fcore\u002Frxjs-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  \u002F\u002F State signals\n  private _user = signal\u003CUser | null>(null);\n  private _loading = signal(false);\n  private _error = signal\u003Cstring | null>(null);\n\n  \u002F\u002F 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  \u002F\u002F Actions\n  async loadUser(id: string) {\n    this._loading.set(true);\n    this._error.set(null);\n\n    try {\n      const user = await fetch(`\u002Fapi\u002Fusers\u002F${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\u003CUser>) {\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\u002F\u002F stores\u002Fproducts.store.ts\nimport {\n  signalStore,\n  withState,\n  withMethods,\n  withComputed,\n  patchState,\n} from \"@ngrx\u002Fsignals\";\nimport { inject } from \"@angular\u002Fcore\";\nimport { ProductService } from \".\u002Fproduct.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\u002F\u002F Usage\n@Component({\n  template: `\n    \u003Cinput (input)=\"store.setFilter($event.target.value)\" \u002F>\n    @if (store.loading()) {\n      \u003Capp-spinner \u002F>\n    } @else {\n      @for (product of store.filteredProducts(); track product.id) {\n        \u003Capp-product-card [product]=\"product\" \u002F>\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\u002F\u002F store\u002Fapp.state.ts\nimport { ActionReducerMap } from \"@ngrx\u002Fstore\";\n\nexport interface AppState {\n  user: UserState;\n  cart: CartState;\n}\n\nexport const reducers: ActionReducerMap\u003CAppState> = {\n  user: userReducer,\n  cart: cartReducer,\n};\n\n\u002F\u002F 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\u002F\u002F store\u002Fuser\u002Fuser.actions.ts\nimport { createActionGroup, props, emptyProps } from \"@ngrx\u002Fstore\";\n\nexport const UserActions = createActionGroup({\n  source: \"User\",\n  events: {\n    \"Load User\": props\u003C{ userId: string }>(),\n    \"Load User Success\": props\u003C{ user: User }>(),\n    \"Load User Failure\": props\u003C{ error: string }>(),\n    \"Update User\": props\u003C{ updates: Partial\u003CUser> }>(),\n    Logout: emptyProps(),\n  },\n});\n```\n\n```typescript\n\u002F\u002F store\u002Fuser\u002Fuser.reducer.ts\nimport { createReducer, on } from \"@ngrx\u002Fstore\";\nimport { UserActions } from \".\u002Fuser.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\u002F\u002F store\u002Fuser\u002Fuser.selectors.ts\nimport { createFeatureSelector, createSelector } from \"@ngrx\u002Fstore\";\nimport { UserState } from \".\u002Fuser.reducer\";\n\nexport const selectUserState = createFeatureSelector\u003CUserState>(\"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\u002F\u002F store\u002Fuser\u002Fuser.effects.ts\nimport { Injectable, inject } from \"@angular\u002Fcore\";\nimport { Actions, createEffect, ofType } from \"@ngrx\u002Feffects\";\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      \u003Capp-spinner \u002F>\n    } @else if (user(); as user) {\n      \u003Ch1>Welcome, {{ user.name }}\u003C\u002Fh1>\n      \u003Cbutton (click)=\"logout()\">Logout\u003C\u002Fbutton>\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\u002F\u002F stores\u002Ftodo.store.ts\nimport { Injectable } from \"@angular\u002Fcore\";\nimport { ComponentStore } from \"@ngrx\u002Fcomponent-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\u003CTodoState> {\n  constructor(private todoService: TodoService) {\n    super({ todos: [], loading: false });\n  }\n\n  \u002F\u002F 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  \u002F\u002F 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  \u002F\u002F Effects\n  readonly loadTodos = this.effect\u003Cvoid>((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\u002F\u002F services\u002Fapi.service.ts\nimport { Injectable, signal, inject } from \"@angular\u002Fcore\";\nimport { HttpClient } from \"@angular\u002Fcommon\u002Fhttp\";\nimport { toSignal } from \"@angular\u002Fcore\u002Frxjs-interop\";\n\ninterface ApiState\u003CT> {\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\u003CApiState\u003CProduct[]>>({\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\u003Cvoid> {\n    this._state.update((s) => ({ ...s, loading: true, error: null }));\n\n    try {\n      const data = await firstValueFrom(\n        this.http.get\u003CProduct[]>(\"\u002Fapi\u002Fproducts\"),\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  \u002F\u002F Optimistic update\n  async deleteProduct(id: string): Promise\u003Cvoid> {\n    const previousData = this._state().data;\n\n    \u002F\u002F 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(`\u002Fapi\u002Fproducts\u002F${id}`));\n    } catch {\n      \u002F\u002F 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`\u002F`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\u002F\u002F Before: RxJS-based\n@Injectable({ providedIn: \"root\" })\nexport class OldUserService {\n  private userSubject = new BehaviorSubject\u003CUser | null>(null);\n  user$ = this.userSubject.asObservable();\n\n  setUser(user: User) {\n    this.userSubject.next(user);\n  }\n}\n\n\u002F\u002F After: Signal-based\n@Injectable({ providedIn: \"root\" })\nexport class UserService {\n  private _user = signal\u003CUser | 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\u002Fcore\u002Frxjs-interop';\n\n\u002F\u002F Observable → Signal\n@Component({...})\nexport class ExampleComponent {\n  private route = inject(ActivatedRoute);\n\n  \u002F\u002F Convert Observable to Signal\n  userId = toSignal(\n    this.route.params.pipe(map(p => p['id'])),\n    { initialValue: '' }\n  );\n}\n\n\u002F\u002F Signal → Observable\nexport class DataService {\n  private filter = signal('');\n\n  \u002F\u002F Convert Signal to Observable\n  filter$ = toObservable(this.filter);\n\n  filteredData$ = this.filter$.pipe(\n    debounceTime(300),\n    switchMap(filter => this.http.get(`\u002Fapi\u002Fdata?q=${filter}`))\n  );\n}\n```\n\n---\n\n## Resources\n\n- [Angular Signals Guide](https:\u002F\u002Fangular.dev\u002Fguide\u002Fsignals)\n- [NgRx Documentation](https:\u002F\u002Fngrx.io\u002F)\n- [NgRx SignalStore](https:\u002F\u002Fngrx.io\u002Fguide\u002Fsignals)\n- [RxAngular](https:\u002F\u002Fwww.rx-angular.io\u002F)\n",{"name":5,"risk":27,"source":32,"date_added":44,"description":7},"2026-02-27","main","7a38d8e7073e61c16eda85681cb32fb68a8657a5","skills-store\u002Fcursor\u002Fangular-state-management","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router","https:\u002F\u002Fgithub.com\u002Fsapsapshen","2026-04-14 21:12:12",{"source":8,"skillName":5,"commands":52,"prompt":62,"zipAvailable":63},[53,56,59],{"tool":54,"command":55},"npx","npx skills add sapsapshen\u002Fai-memory-router --skill angular-state-management",{"tool":57,"command":58},"bunx","bunx skills add sapsapshen\u002Fai-memory-router --skill angular-state-management",{"tool":60,"command":61},"pnpm","pnpm dlx skills add sapsapshen\u002Fai-memory-router --skill angular-state-management","安装技能，技能地址：https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcursor\u002Fangular-state-management\u002FSKILL.md",true,[65,71],{"path":66,"resourceType":67,"resourceTypeDesc":68,"sizeBytes":69,"sourceHtmlUrl":70},"metadata.json","REFERENCE","参考文档",661,"https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcursor\u002Fangular-state-management\u002Fmetadata.json",{"path":72,"resourceType":67,"resourceTypeDesc":68,"sizeBytes":73,"sourceHtmlUrl":74},"README.md",1433,"https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fcursor\u002Fangular-state-management\u002FREADME.md",[76,99,124,134,147,166],{"skillNo":77,"name":78,"nameZh":6,"description":79,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":80,"categories":81,"keywords":87,"publishedAt":39},"01M71ATQRKG48SH06FQ697T5G9","wechat-search","Search WeChat Official Account articles using OpenClaw's web search and fetch capabilities with compliance-focused design.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fwechat-search-release\u002FSKILL.md",[82],{"categoryNo":83,"categoryName":84,"taxonomyType":85,"taxonomyTypeDesc":86},"01JBSEEDCAT000000000000037","律师与法务","PROFESSION","适用职业",[88,89,90,91,92,93,94,95,96,97,98,33],"wechat search","wechat","search","capabilities","openclaw","design","web search","compliance","web","official","fetch",{"skillNo":100,"name":101,"nameZh":6,"description":102,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":103,"categories":104,"keywords":105,"publishedAt":39},"01M71ATNAVGZB35PFYM2GC5N00","trend-tap","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend tap', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-tap\u002FSKILL.md",[],[106,107,108,109,110,111,90,112,113,114,115,34,116,117,118,119,120,121,122,123],"trend","trend tap","tap","api keys","Bash","time","trending","hacker","aggregator","do","brew","live","data","google","install","read","across","api",{"skillNo":125,"name":126,"nameZh":6,"description":127,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":128,"categories":129,"keywords":130,"publishedAt":39},"01M71ATJ9AM5KM15AA8PS302EB","trend-radar","Real-time trending topics aggregator across 7 platforms (X\u002FTwitter, Reddit, Google Trends, Hacker News, Zhihu, Bilibili, Weibo). Trigger: when user says 'trend', 'trends', 'trend pulse', '热点', '热搜', or asks what is trending\u002Fhot. Do NOT use web_search — this skill already fetches live data from all platforms with zero API keys.","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftrend-radar\u002FSKILL.md",[],[131,132,106,116,117,118,119,110,90,121,133,122,123,34,114,113,112,120,111,109],"trend radar","radar","pulse",{"skillNo":135,"name":136,"nameZh":6,"description":137,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":138,"categories":139,"keywords":140,"publishedAt":39},"01M71ATGF095N3EJM9G8SD0BB5","tech-news-daily","每日科技新闻汇——从全球多个 RSS 源抓取最新科技新闻，翻译（英→中）、去重、排版成每日简报。当用户询问科技新闻、科技新鲜事、每日科技、技术动态、Daily Tech News 或类似表达时触发此技能。","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Ftech-news-daily\u002FSKILL.md",[],[141,142,143,144,145,146],"news","tech","tech news daily","daily","rss","翻译",{"skillNo":148,"name":149,"nameZh":6,"description":150,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":151,"categories":152,"keywords":158,"publishedAt":39},"01M71ATFAEAMQ17M70NQNY7SR8","summarize","Summarize URLs or files with the summarize CLI (web, PDFs, images, audio, YouTube).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsummarize\u002FSKILL.md",[153],{"categoryNo":154,"categoryName":155,"taxonomyType":156,"taxonomyTypeDesc":157},"01JBSEEDCAT000000000000021","资料检索与研究","SCENARIO","使用场景",[149,96,159,160,161,33,116,162,163,164,108,165,120],"Audio","cli","youtube","images","https","files","pdfs",{"skillNo":167,"name":168,"nameZh":6,"description":169,"descriptionZh":6,"repositoryFullName":8,"ownerLogin":9,"ownerAvatarUrl":10,"ownerAccountNo":11,"ownerDisplayName":12,"repositoryNo":13,"starsCount":14,"licenseSpdxId":15,"licenseFileUrl":6,"sourceHtmlUrl":170,"categories":171,"keywords":172,"publishedAt":39},"01M71ATDNS3R0M4H936PACQAWZ","sonoscli","Control Sonos speakers (discover\u002Fstatus\u002Fplay\u002Fvolume\u002Fgroup).","https:\u002F\u002Fgithub.com\u002Fsapsapshen\u002Fai-memory-router\u002Fblob\u002Fmain\u002Fskills-store\u002Fopenclaw\u002Fsonoscli\u002FSKILL.md",[],[168,173,174,175,120,176,163,177,178,179,180,181],"status","play","go","latest","github","discover","control","cmd","volume"]