Tutorials Logic, IN +91 8092939553 info@tutorialslogic.com
FAQs Support
Navigation
Home About Us Contact Us Blogs FAQs
Tutorials
All Tutorials
Services
Academic Projects Resume Writing Interview Questions Website Development
Compiler Tutorials

State Management in Angular

State Management Options

Angular offers several approaches to state management depending on the complexity of your app:

ApproachBest forComplexity
Component state (signals)Local UI stateLow
Service + SignalsShared state across componentsLow“Medium
NgRx Signals StoreLarge apps, complex stateMedium“High
NgRx (classic)Enterprise apps, Redux patternHigh

Pattern 1: Service with Signals (Recommended)

For most apps, a service holding signals is the simplest and most effective approach. No extra libraries needed.

Signal Store Service
import { Injectable, signal, computed } from '@angular/core';

interface Todo {
    id: number;
    text: string;
    done: boolean;
}

@Injectable({ providedIn: 'root' })
export class TodoStore {
    private _todos = signal<Todo[]>([]);

    // Public read-only signals
    readonly todos       = this._todos.asReadonly();
    readonly total       = computed(() => this._todos().length);
    readonly completed   = computed(() => this._todos().filter(t => t.done).length);
    readonly pending     = computed(() => this.total() - this.completed());

    add(text: string) {
        const todo: Todo = { id: Date.now(), text, done: false };
        this._todos.update(list => [...list, todo]);
    }

    toggle(id: number) {
        this._todos.update(list =>
            list.map(t => t.id === id ? { ...t, done: !t.done } : t)
        );
    }

    remove(id: number) {
        this._todos.update(list => list.filter(t => t.id !== id));
    }

    clearCompleted() {
        this._todos.update(list => list.filter(t => !t.done));
    }
}
import { Component, inject, signal } from '@angular/core';
import { TodoStore } from './todo.store';
import { FormsModule } from '@angular/forms';

@Component({
    selector: 'app-todo-list',
    standalone: true,
    imports: [FormsModule],
    template: `
        <h2>Todos ({{ store.pending() }} pending)</h2>

        <input [(ngModel)]="newTodo" (keydown.enter)="add()" placeholder="Add todo..." />
        <button (click)="add()">Add</button>

        @for (todo of store.todos(); track todo.id) {
            <div [class.done]="todo.done">
                <input type="checkbox" [checked]="todo.done" (change)="store.toggle(todo.id)" />
                {{ todo.text }}
                <button (click)="store.remove(todo.id)">✕</button>
            </div>
        }

        @if (store.completed() > 0) {
            <button (click)="store.clearCompleted()">Clear completed</button>
        }
    `
})
export class TodoListComponent {
    store = inject(TodoStore);
    newTodo = signal('');

    add() {
        if (this.newTodo().trim()) {
            this.store.add(this.newTodo());
            this.newTodo.set('');
        }
    }
}

Pattern 2: NgRx SignalStore

NgRx SignalStore (from @ngrx/signals) is a lightweight, signal-based state management library. It's the modern NgRx approach for Angular 17+.

NgRx SignalStore
npm install @ngrx/signals
import { signalStore, withState, withComputed, withMethods } from '@ngrx/signals';
import { computed } from '@angular/core';

interface CounterState {
    count: number;
    step: number;
}

export const CounterStore = signalStore(
    { providedIn: 'root' },
    withState<CounterState>({ count: 0, step: 1 }),
    withComputed(({ count, step }) => ({
        doubled:   computed(() => count() * 2),
        canReset:  computed(() => count() !== 0),
    })),
    withMethods(({ count, step, ...store }) => ({
        increment: () => store.patchState({ count: count() + step() }),
        decrement: () => store.patchState({ count: count() - step() }),
        reset:     () => store.patchState({ count: 0 }),
        setStep:   (s: number) => store.patchState({ step: s }),
    }))
);
import { Component, inject } from '@angular/core';
import { CounterStore } from './counter.store';

@Component({
    selector: 'app-counter',
    standalone: true,
    providers: [CounterStore],  // or use providedIn: 'root' in the store
    template: `
        <p>Count: {{ store.count() }}</p>
        <p>Doubled: {{ store.doubled() }}</p>
        <button (click)="store.increment()">+</button>
        <button (click)="store.decrement()">-</button>
        <button (click)="store.reset()" [disabled]="!store.canReset()">Reset</button>
    `
})
export class CounterComponent {
    store = inject(CounterStore);
}

Pattern 3: Classic NgRx (Redux)

For large enterprise apps that need strict unidirectional data flow, time-travel debugging, and a full audit trail.

NgRx Classic (Actions → Reducer → Selector)
import { createAction, props } from '@ngrx/store';

export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');
export const reset     = createAction('[Counter] Reset');
export const setCount  = createAction('[Counter] Set', props<{ count: number }>());
import { createReducer, on } from '@ngrx/store';
import { increment, decrement, reset, setCount } 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 => ({ ...state, count: 0 })),
    on(setCount,  (state, { count }) => ({ ...state, count }))
);
import { createSelector, createFeatureSelector } from '@ngrx/store';
import { CounterState } from './counter.reducer';

export const selectCounterState = createFeatureSelector<CounterState>('counter');
export const selectCount   = createSelector(selectCounterState, s => s.count);
export const selectDoubled = createSelector(selectCount, count => count * 2);

Ready to Level Up Your Skills?

Explore 500+ free tutorials across 20+ languages and frameworks.