
Angular State Management
FreeMaster state management in Angular with modern patterns.
Free · Opens the source repo
What Angular State Management does
Angular State Management is a comprehensive guide designed to help developers effectively manage application state in Angular projects. It covers a variety of state management patterns, including local state using Signals, shared state between components, and global state management with NgRx and Akita. The skill is particularly useful for those who need to set up state management from scratch, manage component-level stores, or migrate from older state management patterns.
The skill provides clear criteria for selecting the appropriate state management solution based on application size and complexity. For smaller applications, Signal services are recommended, while NgRx is suggested for larger applications with more complex state requirements. The documentation includes practical examples and code snippets that illustrate how to implement these patterns in real-world scenarios, making it easier for developers to adopt modern practices.
In addition to state management solutions, the skill addresses common challenges such as debugging state-related issues and implementing optimistic updates. This makes it a valuable resource for both new and experienced Angular developers looking to enhance their state management strategies. Whether you're building a simple application or a complex enterprise solution, this skill offers the guidance needed to effectively manage your application's state.
When to use it
Use this skill when setting up global state management, choosing between state solutions, or debugging state issues in Angular applications.
When not to use it
This skill is not suitable for tasks unrelated to Angular state management or for managing state in React applications.
What you can build with it
Setting Up Global State Management
Use this skill to establish a robust global state management system in your Angular application, ensuring data consistency across components.
Migrating from Legacy Patterns
If you're transitioning from older state management practices, this skill offers strategies and examples to help you adopt modern solutions seamlessly.
Implementing Optimistic Updates
Learn how to implement optimistic updates in your Angular applications to enhance user experience and responsiveness.
How to install Angular State Management
View source1. Install with the skills CLI
npx skills add sickn33/agentic-awesome-skills/angular-state-management --agent claude-code2. Or install it manually
Download the skill folder and drop it into ~/.claude/skills/ for all projects, or .claude/skills/ to scope it to one repo. Restart Claude Code so it picks up the new skill.
Anthropic's agentic coding CLI, and the reference implementation of Agent Skills. Drop a skill folder into ~/.claude/skills and Claude Code loads it automatically whenever a task matches the skill's description. Claude Code docs
Inside SKILL.md
Written by sickn33Angular 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.
Frequently asked questions about Angular State Management
Similar skills
Playwright Component Testing
Test React and Vue components in isolation with Playwright.
Fluent UI Blazor
Integrate Fluent UI components in Blazor applications effortlessly.
Build MCP App
Create interactive UI widgets for MCP servers.
Web Design Reviewer
Identify and fix design issues in websites efficiently.
Markstream Install
Seamlessly integrate Markstream for Markdown rendering.
GSAP & Framer Scroll Animation
Create advanced scroll animations effortlessly.
