From harness-claude
> Manage application state with NgRx Store (Redux pattern) or NgRx SignalStore for signal-based state — choose the right tool for the complexity level
npx claudepluginhub intense-visions/harness-engineering --plugin harness-claudeThis skill uses the workspace's default tool permissions.
> Manage application state with NgRx Store (Redux pattern) or NgRx SignalStore for signal-based state — choose the right tool for the complexity level
Guides Angular state management with Signals, NgRx, RxJS for local, global, server state. Use for setup, component stores, solution selection, debugging, migrations.
Manages reactive state in Angular 17+ using signal(), computed(), effect(), and toSignal() for fine-grained, zone-free reactivity without manual subscriptions.
Provides expert Angular/TypeScript patterns for standalone components, signals, RxJS, NgRx state management, smart/dumb components, and performance.
Share bugs, ideas, or general feedback.
Manage application state with NgRx Store (Redux pattern) or NgRx SignalStore for signal-based state — choose the right tool for the complexity level
BehaviorSubject chains that have grown hard to maintaincreateAction and props<{}>() — one action per user intent or server event. Namespace with [Feature] EventName convention.createReducer and on(). Reducers must be pure functions — no side effects, no mutation.createSelector for memoized state projections. Selectors compose and cache; never derive state in component templates.createEffect to handle side effects (HTTP, routing, localStorage). Effects listen to actions and dispatch new actions on success/failure.createEntityAdapter from @ngrx/entity for normalized collections (list of records by ID). It generates standard CRUD reducers and selectors.store.dispatch(action) and store.select(selector). Pipe the selector observable through the async pipe or convert with toSignal().// counter.actions.ts
import { createAction, props } from '@ngrx/store';
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset = createAction('[Counter] Reset', props<{ value: number }>());
// counter.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { increment, decrement, reset } from './counter.actions';
export interface CounterState {
count: number;
}
const initialState: CounterState = { count: 0 };
export const counterReducer = createReducer(
initialState,
on(increment, (state) => ({ ...state, count: state.count + 1 })),
on(decrement, (state) => ({ ...state, count: state.count - 1 })),
on(reset, (state, { value }) => ({ ...state, count: value }))
);
// counter.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';
import { CounterState } from './counter.reducer';
const selectCounterState = createFeatureSelector<CounterState>('counter');
export const selectCount = createSelector(selectCounterState, (s) => s.count);
export const selectIsZero = createSelector(selectCount, (count) => count === 0);
// products.effects.ts
import { inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { switchMap, map, catchError, of } from 'rxjs';
import * as ProductActions from './product.actions';
import { ProductService } from './product.service';
export const loadProducts = createEffect(
(actions$ = inject(Actions), productService = inject(ProductService)) =>
actions$.pipe(
ofType(ProductActions.loadProducts),
switchMap(() =>
productService.getAll().pipe(
map((products) => ProductActions.loadProductsSuccess({ products })),
catchError((error) => of(ProductActions.loadProductsFailure({ error: error.message })))
)
)
),
{ functional: true }
);
import { signalStore, withState, withComputed, withMethods } from '@ngrx/signals';
import { computed } from '@angular/core';
import { inject } from '@angular/core';
type CartState = { items: CartItem[]; loading: boolean };
export const CartStore = signalStore(
{ providedIn: 'root' }, // or provide in component for local scope
withState<CartState>({ items: [], loading: false }),
withComputed(({ items }) => ({
totalItems: computed(() => items().length),
totalPrice: computed(() => items().reduce((s, i) => s + i.price, 0)),
})),
withMethods((store, productService = inject(ProductService)) => ({
addItem(item: CartItem) {
patchState(store, { items: [...store.items(), item] });
},
async loadCart() {
patchState(store, { loading: true });
const items = await productService.getCart().toPromise();
patchState(store, { items: items ?? [], loading: false });
},
}))
);
When to use NgRx Store vs SignalStore vs Service:
| Scenario | Recommended |
|---|---|
| Global shared state across many features | NgRx Store |
| Dev Tools, time travel, action logging | NgRx Store |
| Feature-scoped state, self-contained | SignalStore |
| Simple component-local state | signal() in component |
| Shared state, 2-3 components | Service with signals |
Entity adapter pattern: @ngrx/entity normalizes a list of records into { ids: [], entities: {} } for O(1) lookup by ID. It generates addOne, addMany, updateOne, removeOne adapter methods and getAll, getEntities, selectById selectors.
Selector memoization: createSelector caches the last output. If the inputs haven't changed, the projector function is not called. This makes selectors safe to use in templates with OnPush change detection — the observable only emits when the derived value actually changes.
NgRx DevTools: Install @ngrx/store-devtools and open Redux DevTools in Chrome to inspect action history, diff state, and replay actions. Invaluable for debugging complex state transitions.
Action hygiene: One action per intent, not one action per state field. Actions should describe what happened ([Cart] Item Added) not what should change ([Cart] Set Items). This makes the action log human-readable.