ChatGPT Claude Codex CLI Cohere Cursor DeepSeek Gemini GitHub Copilot GLM Grok Kimi Llama MiniMax Mistral OpenAI opencode Skill

angular-state-management

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.

LLM Mart · 0 points · 17 views 1 listing impressions 0 install-command copies
Virus-scanned Reviewed automatically before listing.

Full trust report

Download sickn33-agentic-awesome-skills-skills_angular-state-management-1f67c44.zip · 5 KB
Part of sickn33/agentic-awesome-skills — 427 skills
This skill couldn't be refreshed from GitHub on the last check — you're seeing the last imported snapshot.

Install

skills CLI npx skills add https://github.com/sickn33/agentic-awesome-skills/tree/main/skills/angular-state-management
Claude Code claude plugin marketplace add https://llmmart.ai/marketplace.json && claude plugin install sickn33-agentic-awesome-skills@llmmart
Git git clone https://github.com/sickn33/agentic-awesome-skills.git

The skills CLI installs just this skill, for any of its supported agents. Claude Code installs the whole sickn33/agentic-awesome-skills collection as a plugin from our marketplace. Git is the plain clone.

README

Angular State Management

Complete state management patterns for Angular applications optimized for AI agents and LLMs.

Overview

This skill provides decision frameworks and implementation patterns for:

  • Signal-based Services - Lightweight state for shared data
  • NgRx SignalStore - Feature-scoped state with computed values
  • NgRx Store - Enterprise-scale global state management
  • RxJS ComponentStore - Reactive component-level state
  • Forms State - Reactive and template-driven form patterns

Structure

The SKILL.md file is organized into:

  1. State Categories - Local, shared, global, server, URL, and form state
  2. Selection Criteria - Decision trees for choosing the right solution
  3. Implementation Patterns - Complete examples for each approach
  4. Migration Guides - Moving from BehaviorSubject to Signals
  5. Bridging Patterns - Integrating Signals with RxJS

When to Use Each Pattern

  • Signal Service: Shared UI state (theme, user preferences)
  • NgRx SignalStore: Feature state with computed values
  • NgRx Store: Complex cross-feature dependencies
  • ComponentStore: Component-scoped async operations
  • Reactive Forms: Form state with validation

Version

Current version: 1.0.0 (February 2026)

References

Skill manifest

Angular State Management

Comprehensive guide to modern Angular state management patterns, from Signal-based local state to global stores and server state synchronization.

When to Use This Skill

  • Setting up global state management in Angular
  • Choosing between Signals, NgRx, or Akita
  • Managing component-level stores
  • Implementing optimistic updates
  • Debugging state-related issues
  • Migrating from legacy state patterns

Do Not Use This Skill When

  • The task is unrelated to Angular state management
  • You need React state management → use react-state-management

Core Concepts

State Categories

Type Description Solutions
Local State Component-specific, UI state Signals, signal()
Shared State Between related components Signal services
Global State App-wide, complex NgRx, Akita, Elf
Server State Remote data, caching NgRx Query, RxAngular
URL State Route parameters ActivatedRoute
Form State Input values, validation Reactive Forms

Selection Criteria

Small app, simple state → Signal Services
Medium app, moderate state → Component Stores
Large app, complex state → NgRx Store
Heavy server interaction → NgRx Query + Signal Services
Real-time updates → RxAngular + Signals

Quick Start: Signal-Based State

Pattern 1: Simple Signal Service

// services/counter.service.ts
import { Injectable, signal, computed } from "@angular/core";

@Injectable({ providedIn: "root" })
export class CounterService {
  // Private writable signals
  private _count = signal(0);

  // Public read-only
  readonly count = this._count.asReadonly();
  readonly doubled = computed(() => this._count() * 2);
  readonly isPositive = computed(() => this._count() > 0);

  increment() {
    this._count.update((v) => v + 1);
  }

  decrement() {
    this._count.update((v) => v - 1);
  }

  reset() {
    this._count.set(0);
  }
}

// Usage in component
@Component({
  template: `
    <p>Count: {{ counter.count() }}</p>
    <p>Doubled: {{ counter.doubled() }}</p>
    <button (click)="counter.increment()">+</button>
  `,
})
export class CounterComponent {
  counter = inject(CounterService);
}

Pattern 2: Feature Signal Store

// stores/user.store.ts
import { Injectable, signal, computed, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { toSignal } from "@angular/core/rxjs-interop";

interface User {
  id: string;
  name: string;
  email: string;
}

interface UserState {
  user: User | null;
  loading: boolean;
  error: string | null;
}

@Injectable({ providedIn: "root" })
export class UserStore {
  private http = inject(HttpClient);

  // State signals
  private _user = signal<User | null>(null);
  private _loading = signal(false);
  private _error = signal<string | null>(null);

  // Selectors (read-only computed)
  readonly user = computed(() => this._user());
  readonly loading = computed(() => this._loading());
  readonly error = computed(() => this._error());
  readonly isAuthenticated = computed(() => this._user() !== null);
  readonly displayName = computed(() => this._user()?.name ?? "Guest");

  // Actions
  async loadUser(id: string) {
    this._loading.set(true);
    this._error.set(null);

    try {
      const user = await fetch(`/api/users/${id}`).then((r) => r.json());
      this._user.set(user);
    } catch (e) {
      this._error.set("Failed to load user");
    } finally {
      this._loading.set(false);
    }
  }

  updateUser(updates: Partial<User>) {
    this._user.update((user) => (user ? { ...user, ...updates } : null));
  }

  logout() {
    this._user.set(null);
    this._error.set(null);
  }
}

Pattern 3: SignalStore (NgRx Signals)

// stores/products.store.ts
import {
  signalStore,
  withState,
  withMethods,
  withComputed,
  patchState,
} from "@ngrx/signals";
import { inject } from "@angular/core";
import { ProductService } from "./product.service";

interface ProductState {
  products: Product[];
  loading: boolean;
  filter: string;
}

const initialState: ProductState = {
  products: [],
  loading: false,
  filter: "",
};

export const ProductStore = signalStore(
  { providedIn: "root" },

  withState(initialState),

  withComputed((store) => ({
    filteredProducts: computed(() => {
      const filter = store.filter().toLowerCase();
      return store
        .products()
        .filter((p) => p.name.toLowerCase().includes(filter));
    }),
    totalCount: computed(() => store.products().length),
  })),

  withMethods((store, productService = inject(ProductService)) => ({
    async loadProducts() {
      patchState(store, { loading: true });

      try {
        const products = await productService.getAll();
        patchState(store, { products, loading: false });
      } catch {
        patchState(store, { loading: false });
      }
    },

    setFilter(filter: string) {
      patchState(store, { filter });
    },

    addProduct(product: Product) {
      patchState(store, ({ products }) => ({
        products: [...products, product],
      }));
    },
  })),
);

// Usage
@Component({
  template: `
    <input (input)="store.setFilter($event.target.value)" />
    @if (store.loading()) {
      <app-spinner />
    } @else {
      @for (product of store.filteredProducts(); track product.id) {
        <app-product-card [product]="product" />
      }
    }
  `,
})
export class ProductListComponent {
  store = inject(ProductStore);

  ngOnInit() {
    this.store.loadProducts();
  }
}

NgRx Store (Global State)

Setup

// store/app.state.ts
import { ActionReducerMap } from "@ngrx/store";

export interface AppState {
  user: UserState;
  cart: CartState;
}

export const reducers: ActionReducerMap<AppState> = {
  user: userReducer,
  cart: cartReducer,
};

// main.ts
bootstrapApplication(AppComponent, {
  providers: [
    provideStore(reducers),
    provideEffects([UserEffects, CartEffects]),
    provideStoreDevtools({ maxAge: 25 }),
  ],
});

Feature Slice Pattern

// store/user/user.actions.ts
import { createActionGroup, props, emptyProps } from "@ngrx/store";

export const UserActions = createActionGroup({
  source: "User",
  events: {
    "Load User": props<{ userId: string }>(),
    "Load User Success": props<{ user: User }>(),
    "Load User Failure": props<{ error: string }>(),
    "Update User": props<{ updates: Partial<User> }>(),
    Logout: emptyProps(),
  },
});
// store/user/user.reducer.ts
import { createReducer, on } from "@ngrx/store";
import { UserActions } from "./user.actions";

export interface UserState {
  user: User | null;
  loading: boolean;
  error: string | null;
}

const initialState: UserState = {
  user: null,
  loading: false,
  error: null,
};

export const userReducer = createReducer(
  initialState,

  on(UserActions.loadUser, (state) => ({
    ...state,
    loading: true,
    error: null,
  })),

  on(UserActions.loadUserSuccess, (state, { user }) => ({
    ...state,
    user,
    loading: false,
  })),

  on(UserActions.loadUserFailure, (state, { error }) => ({
    ...state,
    loading: false,
    error,
  })),

  on(UserActions.logout, () => initialState),
);
// store/user/user.selectors.ts
import { createFeatureSelector, createSelector } from "@ngrx/store";
import { UserState } from "./user.reducer";

export const selectUserState = createFeatureSelector<UserState>("user");

export const selectUser = createSelector(
  selectUserState,
  (state) => state.user,
);

export const selectUserLoading = createSelector(
  selectUserState,
  (state) => state.loading,
);

export const selectIsAuthenticated = createSelector(
  selectUser,
  (user) => user !== null,
);
// store/user/user.effects.ts
import { Injectable, inject } from "@angular/core";
import { Actions, createEffect, ofType } from "@ngrx/effects";
import { switchMap, map, catchError, of } from "rxjs";

@Injectable()
export class UserEffects {
  private actions$ = inject(Actions);
  private userService = inject(UserService);

  loadUser$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UserActions.loadUser),
      switchMap(({ userId }) =>
        this.userService.getUser(userId).pipe(
          map((user) => UserActions.loadUserSuccess({ user })),
          catchError((error) =>
            of(UserActions.loadUserFailure({ error: error.message })),
          ),
        ),
      ),
    ),
  );
}

Component Usage

@Component({
  template: `
    @if (loading()) {
      <app-spinner />
    } @else if (user(); as user) {
      <h1>Welcome, {{ user.name }}</h1>
      <button (click)="logout()">Logout</button>
    }
  `,
})
export class HeaderComponent {
  private store = inject(Store);

  user = this.store.selectSignal(selectUser);
  loading = this.store.selectSignal(selectUserLoading);

  logout() {
    this.store.dispatch(UserActions.logout());
  }
}

RxJS-Based Patterns

Component Store (Local Feature State)

// stores/todo.store.ts
import { Injectable } from "@angular/core";
import { ComponentStore } from "@ngrx/component-store";
import { switchMap, tap, catchError, EMPTY } from "rxjs";

interface TodoState {
  todos: Todo[];
  loading: boolean;
}

@Injectable()
export class TodoStore extends ComponentStore<TodoState> {
  constructor(private todoService: TodoService) {
    super({ todos: [], loading: false });
  }

  // Selectors
  readonly todos$ = this.select((state) => state.todos);
  readonly loading$ = this.select((state) => state.loading);
  readonly completedCount$ = this.select(
    this.todos$,
    (todos) => todos.filter((t) => t.completed).length,
  );

  // Updaters
  readonly addTodo = this.updater((state, todo: Todo) => ({
    ...state,
    todos: [...state.todos, todo],
  }));

  readonly toggleTodo = this.updater((state, id: string) => ({
    ...state,
    todos: state.todos.map((t) =>
      t.id === id ? { ...t, completed: !t.completed } : t,
    ),
  }));

  // Effects
  readonly loadTodos = this.effect<void>((trigger$) =>
    trigger$.pipe(
      tap(() => this.patchState({ loading: true })),
      switchMap(() =>
        this.todoService.getAll().pipe(
          tap({
            next: (todos) => this.patchState({ todos, loading: false }),
            error: () => this.patchState({ loading: false }),
          }),
          catchError(() => EMPTY),
        ),
      ),
    ),
  );
}

Server State with Signals

HTTP + Signals Pattern

// services/api.service.ts
import { Injectable, signal, inject } from "@angular/core";
import { HttpClient } from "@angular/common/http";
import { toSignal } from "@angular/core/rxjs-interop";

interface ApiState<T> {
  data: T | null;
  loading: boolean;
  error: string | null;
}

@Injectable({ providedIn: "root" })
export class ProductApiService {
  private http = inject(HttpClient);

  private _state = signal<ApiState<Product[]>>({
    data: null,
    loading: false,
    error: null,
  });

  readonly products = computed(() => this._state().data ?? []);
  readonly loading = computed(() => this._state().loading);
  readonly error = computed(() => this._state().error);

  async fetchProducts(): Promise<void> {
    this._state.update((s) => ({ ...s, loading: true, error: null }));

    try {
      const data = await firstValueFrom(
        this.http.get<Product[]>("/api/products"),
      );
      this._state.update((s) => ({ ...s, data, loading: false }));
    } catch (e) {
      this._state.update((s) => ({
        ...s,
        loading: false,
        error: "Failed to fetch products",
      }));
    }
  }

  // Optimistic update
  async deleteProduct(id: string): Promise<void> {
    const previousData = this._state().data;

    // Optimistically remove
    this._state.update((s) => ({
      ...s,
      data: s.data?.filter((p) => p.id !== id) ?? null,
    }));

    try {
      await firstValueFrom(this.http.delete(`/api/products/${id}`));
    } catch {
      // Rollback on error
      this._state.update((s) => ({ ...s, data: previousData }));
    }
  }
}

Best Practices

Do's

Practice Why
Use Signals for local state Simple, reactive, no subscriptions
Use computed() for derived data Auto-updates, memoized
Colocate state with feature Easier to maintain
Use NgRx for complex flows Actions, effects, devtools
Prefer inject() over constructor Cleaner, works in factories

Don'ts

Anti-Pattern Instead
Store derived data Use computed()
Mutate signals directly Use set() or update()
Over-globalize state Keep local when possible
Mix RxJS and Signals chaotically Choose primary, bridge with toSignal/toObservable
Subscribe in components for state Use template with signals

Migration Path

From BehaviorSubject to Signals

// Before: RxJS-based
@Injectable({ providedIn: "root" })
export class OldUserService {
  private userSubject = new BehaviorSubject<User | null>(null);
  user$ = this.userSubject.asObservable();

  setUser(user: User) {
    this.userSubject.next(user);
  }
}

// After: Signal-based
@Injectable({ providedIn: "root" })
export class UserService {
  private _user = signal<User | null>(null);
  readonly user = this._user.asReadonly();

  setUser(user: User) {
    this._user.set(user);
  }
}

Bridging Signals and RxJS

import { toSignal, toObservable } from '@angular/core/rxjs-interop';

// Observable → Signal
@Component({...})
export class ExampleComponent {
  private route = inject(ActivatedRoute);

  // Convert Observable to Signal
  userId = toSignal(
    this.route.params.pipe(map(p => p['id'])),
    { initialValue: '' }
  );
}

// Signal → Observable
export class DataService {
  private filter = signal('');

  // Convert Signal to Observable
  filter$ = toObservable(this.filter);

  filteredData$ = this.filter$.pipe(
    debounceTime(300),
    switchMap(filter => this.http.get(`/api/data?q=${filter}`))
  );
}

Resources

Limitations

  • Use this skill only when the task clearly matches the scope described above.
  • Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
  • Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
Files (agentic-awesome-skills)
  • metadata.json 657 B
    {
      "version": "1.0.0",
      "organization": "Agentic Awesome Skills",
      "date": "February 2026",
      "abstract": "Complete state management guide for Angular applications designed for AI agents and LLMs. Covers Signal-based services, NgRx for global state, RxJS patterns, and component stores. Includes decision trees for choosing the right solution, migration patterns from BehaviorSubject to Signals, and strategies for bridging Signals with RxJS observables.",
      "references": [
        "https://angular.dev/guide/signals",
        "https://ngrx.io",
        "https://ngrx.io/guide/signals",
        "https://www.rx-angular.io",
        "https://github.com/ngrx/platform"
      ]
    }
    
  • README.md 1.4 KB
    # Angular State Management
    
    Complete state management patterns for Angular applications optimized for AI agents and LLMs.
    
    ## Overview
    
    This skill provides decision frameworks and implementation patterns for:
    
    - **Signal-based Services** - Lightweight state for shared data
    - **NgRx SignalStore** - Feature-scoped state with computed values
    - **NgRx Store** - Enterprise-scale global state management
    - **RxJS ComponentStore** - Reactive component-level state
    - **Forms State** - Reactive and template-driven form patterns
    
    ## Structure
    
    The `SKILL.md` file is organized into:
    
    1. **State Categories** - Local, shared, global, server, URL, and form state
    2. **Selection Criteria** - Decision trees for choosing the right solution
    3. **Implementation Patterns** - Complete examples for each approach
    4. **Migration Guides** - Moving from BehaviorSubject to Signals
    5. **Bridging Patterns** - Integrating Signals with RxJS
    
    ## When to Use Each Pattern
    
    - **Signal Service**: Shared UI state (theme, user preferences)
    - **NgRx SignalStore**: Feature state with computed values
    - **NgRx Store**: Complex cross-feature dependencies
    - **ComponentStore**: Component-scoped async operations
    - **Reactive Forms**: Form state with validation
    
    ## Version
    
    Current version: 1.0.0 (February 2026)
    
    ## References
    
    - [Angular Signals](https://angular.dev/guide/signals)
    - [NgRx](https://ngrx.io)
    - [NgRx SignalStore](https://ngrx.io/guide/signals)
    
  • SKILL.md 15.4 KB
    ---
    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."
    risk: safe
    source: self
    date_added: "2026-02-27"
    ---
    
    # Angular State Management
    
    Comprehensive guide to modern Angular state management patterns, from Signal-based local state to global stores and server state synchronization.
    
    ## When to Use This Skill
    
    - Setting up global state management in Angular
    - Choosing between Signals, NgRx, or Akita
    - Managing component-level stores
    - Implementing optimistic updates
    - Debugging state-related issues
    - Migrating from legacy state patterns
    
    ## Do Not Use This Skill When
    
    - The task is unrelated to Angular state management
    - You need React state management → use `react-state-management`
    
    ---
    
    ## Core Concepts
    
    ### State Categories
    
    | Type             | Description                  | Solutions             |
    | ---------------- | ---------------------------- | --------------------- |
    | **Local State**  | Component-specific, UI state | Signals, `signal()`   |
    | **Shared State** | Between related components   | Signal services       |
    | **Global State** | App-wide, complex            | NgRx, Akita, Elf      |
    | **Server State** | Remote data, caching         | NgRx Query, RxAngular |
    | **URL State**    | Route parameters             | ActivatedRoute        |
    | **Form State**   | Input values, validation     | Reactive Forms        |
    
    ### Selection Criteria
    
    ```
    Small app, simple state → Signal Services
    Medium app, moderate state → Component Stores
    Large app, complex state → NgRx Store
    Heavy server interaction → NgRx Query + Signal Services
    Real-time updates → RxAngular + Signals
    ```
    
    ---
    
    ## Quick Start: Signal-Based State
    
    ### Pattern 1: Simple Signal Service
    
    ```typescript
    // services/counter.service.ts
    import { Injectable, signal, computed } from "@angular/core";
    
    @Injectable({ providedIn: "root" })
    export class CounterService {
      // Private writable signals
      private _count = signal(0);
    
      // Public read-only
      readonly count = this._count.asReadonly();
      readonly doubled = computed(() => this._count() * 2);
      readonly isPositive = computed(() => this._count() > 0);
    
      increment() {
        this._count.update((v) => v + 1);
      }
    
      decrement() {
        this._count.update((v) => v - 1);
      }
    
      reset() {
        this._count.set(0);
      }
    }
    
    // Usage in component
    @Component({
      template: `
        <p>Count: {{ counter.count() }}</p>
        <p>Doubled: {{ counter.doubled() }}</p>
        <button (click)="counter.increment()">+</button>
      `,
    })
    export class CounterComponent {
      counter = inject(CounterService);
    }
    ```
    
    ### Pattern 2: Feature Signal Store
    
    ```typescript
    // stores/user.store.ts
    import { Injectable, signal, computed, inject } from "@angular/core";
    import { HttpClient } from "@angular/common/http";
    import { toSignal } from "@angular/core/rxjs-interop";
    
    interface User {
      id: string;
      name: string;
      email: string;
    }
    
    interface UserState {
      user: User | null;
      loading: boolean;
      error: string | null;
    }
    
    @Injectable({ providedIn: "root" })
    export class UserStore {
      private http = inject(HttpClient);
    
      // State signals
      private _user = signal<User | null>(null);
      private _loading = signal(false);
      private _error = signal<string | null>(null);
    
      // Selectors (read-only computed)
      readonly user = computed(() => this._user());
      readonly loading = computed(() => this._loading());
      readonly error = computed(() => this._error());
      readonly isAuthenticated = computed(() => this._user() !== null);
      readonly displayName = computed(() => this._user()?.name ?? "Guest");
    
      // Actions
      async loadUser(id: string) {
        this._loading.set(true);
        this._error.set(null);
    
        try {
          const user = await fetch(`/api/users/${id}`).then((r) => r.json());
          this._user.set(user);
        } catch (e) {
          this._error.set("Failed to load user");
        } finally {
          this._loading.set(false);
        }
      }
    
      updateUser(updates: Partial<User>) {
        this._user.update((user) => (user ? { ...user, ...updates } : null));
      }
    
      logout() {
        this._user.set(null);
        this._error.set(null);
      }
    }
    ```
    
    ### Pattern 3: SignalStore (NgRx Signals)
    
    ```typescript
    // stores/products.store.ts
    import {
      signalStore,
      withState,
      withMethods,
      withComputed,
      patchState,
    } from "@ngrx/signals";
    import { inject } from "@angular/core";
    import { ProductService } from "./product.service";
    
    interface ProductState {
      products: Product[];
      loading: boolean;
      filter: string;
    }
    
    const initialState: ProductState = {
      products: [],
      loading: false,
      filter: "",
    };
    
    export const ProductStore = signalStore(
      { providedIn: "root" },
    
      withState(initialState),
    
      withComputed((store) => ({
        filteredProducts: computed(() => {
          const filter = store.filter().toLowerCase();
          return store
            .products()
            .filter((p) => p.name.toLowerCase().includes(filter));
        }),
        totalCount: computed(() => store.products().length),
      })),
    
      withMethods((store, productService = inject(ProductService)) => ({
        async loadProducts() {
          patchState(store, { loading: true });
    
          try {
            const products = await productService.getAll();
            patchState(store, { products, loading: false });
          } catch {
            patchState(store, { loading: false });
          }
        },
    
        setFilter(filter: string) {
          patchState(store, { filter });
        },
    
        addProduct(product: Product) {
          patchState(store, ({ products }) => ({
            products: [...products, product],
          }));
        },
      })),
    );
    
    // Usage
    @Component({
      template: `
        <input (input)="store.setFilter($event.target.value)" />
        @if (store.loading()) {
          <app-spinner />
        } @else {
          @for (product of store.filteredProducts(); track product.id) {
            <app-product-card [product]="product" />
          }
        }
      `,
    })
    export class ProductListComponent {
      store = inject(ProductStore);
    
      ngOnInit() {
        this.store.loadProducts();
      }
    }
    ```
    
    ---
    
    ## NgRx Store (Global State)
    
    ### Setup
    
    ```typescript
    // store/app.state.ts
    import { ActionReducerMap } from "@ngrx/store";
    
    export interface AppState {
      user: UserState;
      cart: CartState;
    }
    
    export const reducers: ActionReducerMap<AppState> = {
      user: userReducer,
      cart: cartReducer,
    };
    
    // main.ts
    bootstrapApplication(AppComponent, {
      providers: [
        provideStore(reducers),
        provideEffects([UserEffects, CartEffects]),
        provideStoreDevtools({ maxAge: 25 }),
      ],
    });
    ```
    
    ### Feature Slice Pattern
    
    ```typescript
    // store/user/user.actions.ts
    import { createActionGroup, props, emptyProps } from "@ngrx/store";
    
    export const UserActions = createActionGroup({
      source: "User",
      events: {
        "Load User": props<{ userId: string }>(),
        "Load User Success": props<{ user: User }>(),
        "Load User Failure": props<{ error: string }>(),
        "Update User": props<{ updates: Partial<User> }>(),
        Logout: emptyProps(),
      },
    });
    ```
    
    ```typescript
    // store/user/user.reducer.ts
    import { createReducer, on } from "@ngrx/store";
    import { UserActions } from "./user.actions";
    
    export interface UserState {
      user: User | null;
      loading: boolean;
      error: string | null;
    }
    
    const initialState: UserState = {
      user: null,
      loading: false,
      error: null,
    };
    
    export const userReducer = createReducer(
      initialState,
    
      on(UserActions.loadUser, (state) => ({
        ...state,
        loading: true,
        error: null,
      })),
    
      on(UserActions.loadUserSuccess, (state, { user }) => ({
        ...state,
        user,
        loading: false,
      })),
    
      on(UserActions.loadUserFailure, (state, { error }) => ({
        ...state,
        loading: false,
        error,
      })),
    
      on(UserActions.logout, () => initialState),
    );
    ```
    
    ```typescript
    // store/user/user.selectors.ts
    import { createFeatureSelector, createSelector } from "@ngrx/store";
    import { UserState } from "./user.reducer";
    
    export const selectUserState = createFeatureSelector<UserState>("user");
    
    export const selectUser = createSelector(
      selectUserState,
      (state) => state.user,
    );
    
    export const selectUserLoading = createSelector(
      selectUserState,
      (state) => state.loading,
    );
    
    export const selectIsAuthenticated = createSelector(
      selectUser,
      (user) => user !== null,
    );
    ```
    
    ```typescript
    // store/user/user.effects.ts
    import { Injectable, inject } from "@angular/core";
    import { Actions, createEffect, ofType } from "@ngrx/effects";
    import { switchMap, map, catchError, of } from "rxjs";
    
    @Injectable()
    export class UserEffects {
      private actions$ = inject(Actions);
      private userService = inject(UserService);
    
      loadUser$ = createEffect(() =>
        this.actions$.pipe(
          ofType(UserActions.loadUser),
          switchMap(({ userId }) =>
            this.userService.getUser(userId).pipe(
              map((user) => UserActions.loadUserSuccess({ user })),
              catchError((error) =>
                of(UserActions.loadUserFailure({ error: error.message })),
              ),
            ),
          ),
        ),
      );
    }
    ```
    
    ### Component Usage
    
    ```typescript
    @Component({
      template: `
        @if (loading()) {
          <app-spinner />
        } @else if (user(); as user) {
          <h1>Welcome, {{ user.name }}</h1>
          <button (click)="logout()">Logout</button>
        }
      `,
    })
    export class HeaderComponent {
      private store = inject(Store);
    
      user = this.store.selectSignal(selectUser);
      loading = this.store.selectSignal(selectUserLoading);
    
      logout() {
        this.store.dispatch(UserActions.logout());
      }
    }
    ```
    
    ---
    
    ## RxJS-Based Patterns
    
    ### Component Store (Local Feature State)
    
    ```typescript
    // stores/todo.store.ts
    import { Injectable } from "@angular/core";
    import { ComponentStore } from "@ngrx/component-store";
    import { switchMap, tap, catchError, EMPTY } from "rxjs";
    
    interface TodoState {
      todos: Todo[];
      loading: boolean;
    }
    
    @Injectable()
    export class TodoStore extends ComponentStore<TodoState> {
      constructor(private todoService: TodoService) {
        super({ todos: [], loading: false });
      }
    
      // Selectors
      readonly todos$ = this.select((state) => state.todos);
      readonly loading$ = this.select((state) => state.loading);
      readonly completedCount$ = this.select(
        this.todos$,
        (todos) => todos.filter((t) => t.completed).length,
      );
    
      // Updaters
      readonly addTodo = this.updater((state, todo: Todo) => ({
        ...state,
        todos: [...state.todos, todo],
      }));
    
      readonly toggleTodo = this.updater((state, id: string) => ({
        ...state,
        todos: state.todos.map((t) =>
          t.id === id ? { ...t, completed: !t.completed } : t,
        ),
      }));
    
      // Effects
      readonly loadTodos = this.effect<void>((trigger$) =>
        trigger$.pipe(
          tap(() => this.patchState({ loading: true })),
          switchMap(() =>
            this.todoService.getAll().pipe(
              tap({
                next: (todos) => this.patchState({ todos, loading: false }),
                error: () => this.patchState({ loading: false }),
              }),
              catchError(() => EMPTY),
            ),
          ),
        ),
      );
    }
    ```
    
    ---
    
    ## Server State with Signals
    
    ### HTTP + Signals Pattern
    
    ```typescript
    // services/api.service.ts
    import { Injectable, signal, inject } from "@angular/core";
    import { HttpClient } from "@angular/common/http";
    import { toSignal } from "@angular/core/rxjs-interop";
    
    interface ApiState<T> {
      data: T | null;
      loading: boolean;
      error: string | null;
    }
    
    @Injectable({ providedIn: "root" })
    export class ProductApiService {
      private http = inject(HttpClient);
    
      private _state = signal<ApiState<Product[]>>({
        data: null,
        loading: false,
        error: null,
      });
    
      readonly products = computed(() => this._state().data ?? []);
      readonly loading = computed(() => this._state().loading);
      readonly error = computed(() => this._state().error);
    
      async fetchProducts(): Promise<void> {
        this._state.update((s) => ({ ...s, loading: true, error: null }));
    
        try {
          const data = await firstValueFrom(
            this.http.get<Product[]>("/api/products"),
          );
          this._state.update((s) => ({ ...s, data, loading: false }));
        } catch (e) {
          this._state.update((s) => ({
            ...s,
            loading: false,
            error: "Failed to fetch products",
          }));
        }
      }
    
      // Optimistic update
      async deleteProduct(id: string): Promise<void> {
        const previousData = this._state().data;
    
        // Optimistically remove
        this._state.update((s) => ({
          ...s,
          data: s.data?.filter((p) => p.id !== id) ?? null,
        }));
    
        try {
          await firstValueFrom(this.http.delete(`/api/products/${id}`));
        } catch {
          // Rollback on error
          this._state.update((s) => ({ ...s, data: previousData }));
        }
      }
    }
    ```
    
    ---
    
    ## Best Practices
    
    ### Do's
    
    | Practice                           | Why                                |
    | ---------------------------------- | ---------------------------------- |
    | Use Signals for local state        | Simple, reactive, no subscriptions |
    | Use `computed()` for derived data  | Auto-updates, memoized             |
    | Colocate state with feature        | Easier to maintain                 |
    | Use NgRx for complex flows         | Actions, effects, devtools         |
    | Prefer `inject()` over constructor | Cleaner, works in factories        |
    
    ### Don'ts
    
    | Anti-Pattern                      | Instead                                               |
    | --------------------------------- | ----------------------------------------------------- |
    | Store derived data                | Use `computed()`                                      |
    | Mutate signals directly           | Use `set()` or `update()`                             |
    | Over-globalize state              | Keep local when possible                              |
    | Mix RxJS and Signals chaotically  | Choose primary, bridge with `toSignal`/`toObservable` |
    | Subscribe in components for state | Use template with signals                             |
    
    ---
    
    ## Migration Path
    
    ### From BehaviorSubject to Signals
    
    ```typescript
    // Before: RxJS-based
    @Injectable({ providedIn: "root" })
    export class OldUserService {
      private userSubject = new BehaviorSubject<User | null>(null);
      user$ = this.userSubject.asObservable();
    
      setUser(user: User) {
        this.userSubject.next(user);
      }
    }
    
    // After: Signal-based
    @Injectable({ providedIn: "root" })
    export class UserService {
      private _user = signal<User | null>(null);
      readonly user = this._user.asReadonly();
    
      setUser(user: User) {
        this._user.set(user);
      }
    }
    ```
    
    ### Bridging Signals and RxJS
    
    ```typescript
    import { toSignal, toObservable } from '@angular/core/rxjs-interop';
    
    // Observable → Signal
    @Component({...})
    export class ExampleComponent {
      private route = inject(ActivatedRoute);
    
      // Convert Observable to Signal
      userId = toSignal(
        this.route.params.pipe(map(p => p['id'])),
        { initialValue: '' }
      );
    }
    
    // Signal → Observable
    export class DataService {
      private filter = signal('');
    
      // Convert Signal to Observable
      filter$ = toObservable(this.filter);
    
      filteredData$ = this.filter$.pipe(
        debounceTime(300),
        switchMap(filter => this.http.get(`/api/data?q=${filter}`))
      );
    }
    ```
    
    ---
    
    ## Resources
    
    - [Angular Signals Guide](https://angular.dev/guide/signals)
    - [NgRx Documentation](https://ngrx.io/)
    - [NgRx SignalStore](https://ngrx.io/guide/signals)
    - [RxAngular](https://www.rx-angular.io/)
    
    ## Limitations
    - Use this skill only when the task clearly matches the scope described above.
    - Do not treat the output as a substitute for environment-specific validation, testing, or expert review.
    - Stop and ask for clarification if required inputs, permissions, safety boundaries, or success criteria are missing.
    

Comments (0)

Sign in to join the conversation.

No comments yet.

Reviews (0)

No reviews yet.

Related