home / skills / pluginagentmarketplace / custom-plugin-angular / state-management

state-management skill

/skills/state-management

This skill helps you implement NgRx store architecture with actions, reducers, effects, and selectors for scalable Angular state management.

npx playbooks add skill pluginagentmarketplace/custom-plugin-angular --skill state-management

Review the files below or copy the command above to add this skill to your agents.

Files (10)
SKILL.md
10.1 KB
---
name: state-implementation
description: Implement NgRx store with actions and reducers, build selectors, create effects for async operations, configure entity adapters, and integrate HTTP APIs with state management.
sasmp_version: "1.3.0"
bonded_agent: 06-state-management
bond_type: PRIMARY_BOND
---

# State Implementation Skill

## Quick Start

### Simple Service-Based State
```typescript
import { Injectable } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs';

@Injectable({ providedIn: 'root' })
export class UserStore {
  private usersSubject = new BehaviorSubject<User[]>([]);
  users$ = this.usersSubject.asObservable();

  constructor(private http: HttpClient) {}

  loadUsers() {
    this.http.get<User[]>('/api/users').subscribe(
      users => this.usersSubject.next(users)
    );
  }

  addUser(user: User) {
    this.http.post<User>('/api/users', user).subscribe(
      newUser => {
        const current = this.usersSubject.value;
        this.usersSubject.next([...current, newUser]);
      }
    );
  }
}

// Usage
export class UserListComponent {
  users$ = this.userStore.users$;

  constructor(private userStore: UserStore) {}
}
```

### NgRx Basics
```typescript
// 1. Define actions
export const loadUsers = createAction('[User] Load Users');
export const loadUsersSuccess = createAction(
  '[User] Load Users Success',
  props<{ users: User[] }>()
);
export const loadUsersError = createAction(
  '[User] Load Users Error',
  props<{ error: string }>()
);

// 2. Create reducer
const initialState: UserState = { users: [], loading: false };

export const userReducer = createReducer(
  initialState,
  on(loadUsers, state => ({ ...state, loading: true })),
  on(loadUsersSuccess, (state, { users }) => ({
    ...state,
    users,
    loading: false
  })),
  on(loadUsersError, (state, { error }) => ({
    ...state,
    error,
    loading: false
  }))
);

// 3. Create effect
@Injectable()
export class UserEffects {
  loadUsers$ = createEffect(() =>
    this.actions$.pipe(
      ofType(loadUsers),
      switchMap(() =>
        this.userService.getUsers().pipe(
          map(users => loadUsersSuccess({ users })),
          catchError(error => of(loadUsersError({ error })))
        )
      )
    )
  );

  constructor(
    private actions$: Actions,
    private userService: UserService
  ) {}
}

// 4. Use in component
@Component({...})
export class UserListComponent {
  users$ = this.store.select(selectUsers);
  loading$ = this.store.select(selectLoading);

  constructor(private store: Store) {
    this.store.dispatch(loadUsers());
  }
}
```

## NgRx Core Concepts

### Store
```typescript
// Dispatch action
this.store.dispatch(loadUsers());

// Select state
this.store.select(selectUsers).subscribe(users => {
  console.log(users);
});

// Select with observable
this.users$ = this.store.select(selectUsers);

// Multiple selects
this.store.select(selectUsers, selectLoading).subscribe(([users, loading]) => {
  // ...
});
```

### Selectors
```typescript
// Feature selector
export const selectUserState = createFeatureSelector<UserState>('users');

// Select from feature
export const selectUsers = createSelector(
  selectUserState,
  state => state.users
);

// Selector composition
export const selectActiveUsers = createSelector(
  selectUsers,
  users => users.filter(u => u.active)
);

// Memoized selector
export const selectUserById = (id: number) => createSelector(
  selectUsers,
  users => users.find(u => u.id === id)
);

// With props
export const selectUsersByRole = createSelector(
  selectUsers,
  (users: User[], { role }: { role: string }) =>
    users.filter(u => u.role === role)
);

// Usage with props
this.store.select(selectUsersByRole, { role: 'admin' });
```

### Effects
```typescript
// Side effect - HTTP call
@Injectable()
export class UserEffects {
  loadUsers$ = createEffect(() =>
    this.actions$.pipe(
      ofType(UserActions.loadUsers),
      switchMap(() =>
        this.userService.getUsers().pipe(
          map(users => UserActions.loadUsersSuccess({ users })),
          catchError(error => of(UserActions.loadUsersError({ error })))
        )
      )
    )
  );

  // Non-dispatching effect
  logActions$ = createEffect(
    () => this.actions$.pipe(
      tap(action => console.log(action))
    ),
    { dispatch: false }
  );

  constructor(
    private actions$: Actions,
    private userService: UserService
  ) {}
}
```

## Entity Adapter

### Setup
```typescript
export interface User {
  id: number;
  name: string;
  email: string;
}

export const adapter = createEntityAdapter<User>({
  selectId: (user: User) => user.id,
  sortComparer: (a: User, b: User) => a.name.localeCompare(b.name)
});

export interface UserState extends EntityState<User> {
  loading: boolean;
  error: string | null;
}

const initialState = adapter.getInitialState({
  loading: false,
  error: null
});
```

### Reducer with Adapter
```typescript
export const userReducer = createReducer(
  initialState,
  on(loadUsers, state => ({ ...state, loading: true })),
  on(loadUsersSuccess, (state, { users }) =>
    adapter.setAll(users, { ...state, loading: false })
  ),
  on(addUserSuccess, (state, { user }) =>
    adapter.addOne(user, state)
  ),
  on(updateUserSuccess, (state, { user }) =>
    adapter.updateOne({ id: user.id, changes: user }, state)
  ),
  on(deleteUserSuccess, (state, { id }) =>
    adapter.removeOne(id, state)
  )
);

// Export selectors
export const {
  selectIds,
  selectEntities,
  selectAll,
  selectTotal
} = adapter.getSelectors(selectUserState);
```

## Facade Pattern

```typescript
@Injectable()
export class UserFacade {
  users$ = this.store.select(selectAllUsers);
  loading$ = this.store.select(selectUsersLoading);
  error$ = this.store.select(selectUsersError);

  constructor(private store: Store) {}

  loadUsers() {
    this.store.dispatch(loadUsers());
  }

  addUser(user: User) {
    this.store.dispatch(addUser({ user }));
  }

  updateUser(id: number, changes: Partial<User>) {
    this.store.dispatch(updateUser({ id, changes }));
  }

  deleteUser(id: number) {
    this.store.dispatch(deleteUser({ id }));
  }
}

// Component usage simplified
@Component({...})
export class UserListComponent {
  users$ = this.userFacade.users$;
  loading$ = this.userFacade.loading$;

  constructor(private userFacade: UserFacade) {
    this.userFacade.loadUsers();
  }
}
```

## Angular Signals

```typescript
import { signal, computed, effect } from '@angular/core';

// Create signal
const count = signal(0);

// Read value
console.log(count()); // 0

// Update value
count.set(1);
count.update(c => c + 1);

// Computed value
const doubled = computed(() => count() * 2);

// Effect
effect(() => {
  console.log(`Count is ${count()}`);
  console.log(`Doubled is ${doubled()}`);
});

// Signal-based state
@Component({...})
export class CounterComponent {
  count = signal(0);
  doubled = computed(() => this.count() * 2);

  increment() {
    this.count.update(c => c + 1);
  }
}
```

## HTTP Integration

### HttpClient with Interceptor
```typescript
@Injectable()
export class AuthInterceptor implements HttpInterceptor {
  constructor(private authService: AuthService) {}

  intercept(req: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> {
    const token = this.authService.getToken();
    const authReq = req.clone({
      setHeaders: {
        Authorization: `Bearer ${token}`
      }
    });
    return next.handle(authReq);
  }
}

// Register
@NgModule({
  providers: [
    {
      provide: HTTP_INTERCEPTORS,
      useClass: AuthInterceptor,
      multi: true
    }
  ]
})
export class AppModule { }
```

### Caching Strategy
```typescript
@Injectable()
export class CachingService {
  private cache = new Map<string, any>();

  get<T>(key: string, request: Observable<T>, ttl: number = 3600000): Observable<T> {
    if (this.cache.has(key)) {
      return of(this.cache.get(key));
    }

    return request.pipe(
      tap(data => {
        this.cache.set(key, data);
        setTimeout(() => this.cache.delete(key), ttl);
      })
    );
  }
}

// Usage
getUsers() {
  return this.caching.get(
    'users',
    this.http.get<User[]>('/api/users'),
    5 * 60 * 1000 // 5 minutes
  );
}
```

## Testing State

```typescript
describe('User Store', () => {
  let store: MockStore;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [StoreModule.forRoot({ users: userReducer })]
    });
    store = TestBed.inject(Store) as MockStore;
  });

  it('should load users', () => {
    const action = loadUsers();
    const completion = loadUsersSuccess({ users: mockUsers });

    const effect$ = new UserEffects(
      hot('a', { a: action }),
      mockUserService
    ).loadUsers$;

    const result = cold('b', { b: completion });
    expect(effect$).toBeObservable(result);
  });

  it('should select users', (done) => {
    store.setState({ users: { users: mockUsers } });
    store.select(selectUsers).subscribe(users => {
      expect(users).toEqual(mockUsers);
      done();
    });
  });
});
```

## Best Practices

1. **Normalize State**: Flat structure, avoid nesting
2. **Single Responsibility**: Each reducer handles one feature
3. **Use Facades**: Simplify component-store interaction
4. **Memoize Selectors**: Prevent unnecessary recalculations
5. **Handle Errors**: Always include error states
6. **Lazy Load Stores**: Register feature stores when needed
7. **Time-Travel Debugging**: Use Redux DevTools

## Advanced Patterns

### Composition Pattern
```typescript
// Combine multiple stores
@Injectable()
export class AppFacade {
  users$ = this.userFacade.users$;
  products$ = this.productFacade.products$;
  cart$ = this.cartFacade.cart$;

  constructor(
    private userFacade: UserFacade,
    private productFacade: ProductFacade,
    private cartFacade: CartFacade
  ) {}
}
```

### Feature Flags
```typescript
export const selectFeatureFlags = createFeatureSelector<FeatureFlags>('features');
export const selectFeatureEnabled = (feature: string) => createSelector(
  selectFeatureFlags,
  flags => flags[feature]?.enabled ?? false
);

// Component
<div *ngIf="featureEnabled$ | async">New Feature</div>
```

## Resources

- [NgRx Documentation](https://ngrx.io/)
- [Entity Adapter](https://ngrx.io/guide/entity)
- [DevTools](https://github.com/reduxjs/redux-devtools-extension)

Overview

This skill implements a robust NgRx-based state layer for Angular apps, including actions, reducers, selectors, effects, entity adapters, and HTTP integration. It provides patterns for service-based stores, facades, signal mixing, caching, and testable effects so teams can manage normalized state and async flows consistently.

How this skill works

It defines typed actions and feature reducers to represent state transitions, then exposes selectors for efficient, memoized reads. Effects handle side effects and HTTP calls, returning success or error actions. Entity adapters normalize collections and provide common selectors; facades wrap store interactions for components and make testing easier.

When to use it

  • When you need predictable, testable global state for complex UIs
  • When managing collections that benefit from normalization and ID-based operations
  • When coordinating HTTP requests, optimistic updates, or complex async flows
  • When building reusable feature modules with lazy-loaded state
  • When you want time-travel debugging and clear separation of side effects

Best practices

  • Normalize state shape; prefer flat structures and entity adapters for collections
  • Keep reducers focused on one feature and use actions for intent, not implementation
  • Expose a facade per feature to simplify component code and decouple from NgRx APIs
  • Memoize selectors and compose them to avoid unnecessary recalculations
  • Always include loading and error state for async flows and handle errors in effects
  • Register feature stores lazily when possible and use interceptors for cross-cutting HTTP concerns

Example use cases

  • User management: load, add, update, delete users with adapter-backed reducers and selectors
  • Product catalog: normalized product entities, filters via selectors, and cached HTTP requests
  • Auth flow: interceptors attach tokens, effects handle login/logout flows and persistence
  • Admin dashboards: combine facades from multiple features into a single AppFacade for composed views
  • Offline caching: use a caching service with TTL for repeatable API calls inside effects

FAQ

How do effects report errors back to the UI?

Effects map HTTP errors to failure actions (e.g., loadUsersError) which update error state in the reducer; components subscribe to error selectors or facade observables.

When should I use an entity adapter?

Use an entity adapter when you manage collections keyed by ID and need efficient add/update/remove operations and ready-to-use selectors like selectAll and selectEntities.