#angular #ngrx #signals #state-management

Day 16 — State Management with NgRx SignalStore

📘 Day 16 — State Management with NgRx SignalStore

Zero to Hero — Hands-on Angular Tutorial

Today you will learn:

  • ✔️ Why use a State Library vs. Manual Services?
  • ✔️ NgRx SignalStore: The modern, lightweight alternative to Redux
  • ✔️ signalStore, withState, withMethods, withComputed
  • ✔️ Modifying state with patchState
  • ✔️ Using the store in Components

In Day 7, you built a manual store. Today, we effectively “Go Pro” using the official NgRx SignalStore library—the new standard for Angular state management. 🚀


🟦 1. Why NgRx SignalStore?

You can manage state manually (like we did in Day 7), but SignalStore gives you:

  1. Standardization: A structure every developer understands.
  2. Immutability: patchState handles updates safely.
  3. Extensibility: Add plugins for LocalStorage, Entity Management, etc., easily.
  4. DevTools: Easier debugging.

It is simpler than the old Redux (Actions/Reducers) pattern but just as powerful for 90% of apps.


🟩 2. Installation

npm install @ngrx/signals

🟧 3. Creating Your First SignalStore

Let’s convert our “Counter” or “User” logic into a formal Store.

user.store.ts

import { signalStore, withState, withMethods, withComputed, patchState } from '@ngrx/signals';
import { computed } from '@angular/core';

type UserState = {
  name: string;
  isAdmin: boolean;
  score: number;
};

const initialState: UserState = {
  name: 'Guest',
  isAdmin: false,
  score: 0
};

export const UserStore = signalStore(
  { providedIn: 'root' }, // Makes it injectable everywhere
  withState(initialState),
  
  // 1. Computed Values (Derived State)
  withComputed(({ score, isAdmin }) => ({
    badge: computed(() => (score() > 50 ? 'Gold' : 'Silver')),
    title: computed(() => (isAdmin() ? 'Admin User' : 'Standard User'))
  })),

  // 2. Methods (Actions/Updaters)
  withMethods((store) => ({
    updateName(name: string) {
      patchState(store, { name });
    },
    
    incrementScore() {
      // Functional update (using previous state)
      patchState(store, (state) => ({ score: state.score + 10 }));
    },

    promoteToAdmin() {
      patchState(store, { isAdmin: true });
    },
    
    reset() {
      patchState(store, initialState);
    }
  }))
);

🟥 4. Using the Store in a Component

It works exactly like a Service, but simpler!

user-dashboard.component.ts

import { Component, inject } from '@angular/core';
import { UserStore } from './user.store';

@Component({
  selector: 'app-user-dashboard',
  standalone: true,
  template: `
    <h1>Hello, {{ store.name() }}</h1>
    <p>Rank: {{ store.title() }} ({{ store.badge() }})</p>
    <p>Score: {{ store.score() }}</p>

    <button (click)="store.incrementScore()">Score +10</button>
    <button (click)="store.promoteToAdmin()">Make Admin</button>
    <button (click)="store.reset()">Reset</button>
  `,
  // 👇 INJECT THE STORE HERE
  providers: [UserStore] // Or omit if providedIn: 'root'
})
export class UserDashboardComponent {
  // Inject exactly like a service
  readonly store = inject(UserStore);
}

Notice: No subscribe, no async pipe, no selectors. Just properties.


🟫 5. Async Calls (RxJS Integration)

Stores often need to load data from an API (Side Effects). We use rxMethod for this.

Updates to UserStore:

import { inject } from '@angular/core';
import { rxMethod } from '@ngrx/signals/rxjs-interop';
import { pipe, switchMap, tap } from 'rxjs';
import { HttpClient } from '@angular/common/http';

export const UserStore = signalStore(
  // ... state setup ...
  
  withMethods((store, http = inject(HttpClient)) => ({
    
    // Define an RxJS Method pipeline
    loadUserById: rxMethod<string>(
      pipe(
        switchMap((id) => http.get<any>(`/api/users/${id}`)),
        tap((user) => patchState(store, { 
          name: user.name, 
          score: user.score 
        }))
      )
    )
  }))
);

Usage:

ngOnInit() {
  this.store.loadUserById('123'); // Triggers the API call
}

🟦 6. Lifecycle Hooks in Store

You can run code when the store initializes using withHooks.

import { withHooks } from '@ngrx/signals';

export const UserStore = signalStore(
  // ...
  withHooks({
    onInit(store) {
      console.log('Store initialized!');
      // Good place to load initial data from LocalStorage
    },
    onDestroy(store) {
      console.log('Store destroyed');
    }
  })
);

🎉 End of Day 16 — What You Learned

Today you adopted the industry-standard NgRx SignalStore:

  • ✔️ Structure: State, Computed, Methods, Hooks.
  • ✔️ patchState: The safe way to update data.
  • ✔️ rxMethod: Handling Async/HTTP calls inside the store.
  • ✔️ Simplicity: No reducers, no actions, just code.

This architecture scales from small apps to massive enterprise systems.


🧪 Day 16 Challenge

Build a “Shopping Cart Store” using NgRx SignalStore.

Requirements:

  1. State: items (array), loading (boolean).
  2. Computed: totalPrice, itemCount.
  3. Methods: addItem(item), removeItem(id), checkout().
  4. Async: checkout() should simulate a 2-second API delay, set loading to true, then clear the cart.
  5. Hook: Log “Cart Ready” when the store starts.