NgRX

— by Youcef MADADI, FREELANCER & PROJECT MANAGER

Index for NgRx

Introduction to NgRx

What is NgRx?

  • NgRx is a state management library for Angular applications.

  • Built on RxJS, it uses Reactive Extensions to manage and synchronize application state.

  • Inspired by Redux but tailored for Angular's ecosystem.

  • Centralized State Management:

    • All state is stored in a single source of truth (the store).
  • Predictability:

    • State changes are managed through pure functions called reducers.
  • Scalability:

    • Simplifies managing complex states in large applications.
  • Debugging and DevTools:

    • Time-travel debugging, action history, and more.
  • Reactive Programming:

    • Built on RxJS for powerful and efficient reactive data streams.

Why Use NgRx?

NgRx Key Concepts

Concept Description
Store A centralized container for application state.
State Immutable data representing the current application condition.
Actions Plain objects that describe an intention to change state.
Reducers Pure functions that handle state changes in response to actions.
Effects Handle side effects like API calls and async operations.
Selectors Functions to query specific parts of the state.
  • A lightweight state management solution in NgRx.
  • Built on Angular's Signals API for reactivity.
  • Ideal for local state management within components or small modules.
  • Introduced in NgRx v16 to simplify state management.

Introduction to signalStore

The newest approach of Angular NgRx stores

 Why Use signalStore ?

  • Lightweight and Simple:

    • No need for global state management setup.
  • Encapsulated State:

    • Perfect for feature-specific or component-specific state.
  • Reactive by Design:

    • Integrates seamlessly with Angular's reactive primitives.
  • Flexible Updates:

    • Easily update state using update() without boilerplate.
  • Declarative State Definition:

    • Uses withState() to define initial state.
  • State Updates:

    • Mutate state with an update() function.
  • Reactive Properties:

    • Derive reactive, computed properties using Angular’s computed().
  • Scoped Usage:

    • Perfect for self-contained, local state management.

Key Features of signalStore

How signalStore Works?

import { signalStore, withState } from '@ngrx/signals';

const initialState = {
  items: [],
  isLoading: false,
};

export const ItemsStore = signalStore(
  withState(initialState) // Provide the initial state
);

How signalStore Works?

import { Component, inject } from '@angular/core';
import { ItemsStore } from './items.store';

@Component({
  selector: 'app-items',
  template: `
    <div *ngIf="store.state().isLoading">Loading...</div>
    <ul>
      <li *ngFor="let item of store.state().items">{{ item.name }}</li>
    </ul>
    <button (click)="loadItems()">Load Items</button>
  `,
})
export class ItemsComponent {
  store = inject(ItemsStore);

  loadItems() {
    this.store.update((state) => ({
      ...state,
      isLoading: true,
    }));
  }
}

Using withComputed in signalStore

What is withComputed?

  • A utility in signalStore that adds computed properties to your store.
  • Built on Angular's computed API, enabling derived state.
  • Automatically recalculates when dependent state changes.

Why Use withComputed?

  • Simplify Derived State:

    • No need to manually compute derived values in the component.
  • Reactive Updates:

    • Automatically updates computed properties when the base state changes.
  • Declarative State Logic:

    • Encapsulates logic within the store for better maintainability.

Practical Use Cases

  • E-commerce Cart:

    • Calculate total price or items in the cart.
  • Filters:

    • Derive filtered data based on selected filters.
  • Pagination:

    • Compute derived properties like currentPage or totalPages.

 How Does withComputed Work?

type CartState = {
  items: { name: string; price: number; quantity: number }[];
};
const initialState: CartState = {
  items: [
    { name: 'Book', price: 20, quantity: 2 },
    { name: 'Pen', price: 5, quantity: 4 },
  ],
};
export const CartStore = signalStore(
  withState(initialState),
  withComputed(({ items }) => ({
    totalItems: () =>items().reduce((sum, item) => sum + item.quantity, 0),
    totalPrice: () => items().reduce((sum, item) => sum + item.price * item.quantity, 0),
  }))
);

 How Does withComputed Work?

import { Component, inject } from '@angular/core';
import { CartStore } from './cart.store';

@Component({
  selector: 'app-cart',
  template: `
    <h1>Cart</h1>
    <ul>
      <li *ngFor="let item of store.state().items">
        {{ item.name }} - {{ item.quantity }} x {{ item.price }}$
      </li>
    </ul>
    <p>Total Items: {{ store.computed().totalItems() }}</p>
    <p>Total Price: {{ store.computed().totalPrice() }}$</p>
  `,
})
export class CartComponent {
  store = inject(CartStore);
}

Using withMethods in signalStore

What is withMethods?

  • A utility in signalStore to encapsulate business logic and reusable methods in the store.
  • Allows the store to provide custom, reusable methods for interacting with its state.
  • Keeps components cleaner by centralizing logic in the store.

Why Use withMethods?

  1. Encapsulation:

    • Keeps business logic within the store, avoiding clutter in components.
  2. Reusability:

    • Define methods once and use them across multiple components.
  3. Readability:

    • Components focus on UI logic, while the store manages state and methods.

Practical Use Cases

  • State Management:

    • like actions Increment, decrement, reset, or perform other counter-related actions.
  • CRUD Operations:

    • Add, update, or delete items in a list.
  • Complex State Transitions:

    • Encapsulate multi-step logic for updating state.
  • Reusable Actions:

    • Any repetitive or reusable actions across components.

 How Does withMethods Work?

// Add base state and methods
export const CounterStore = signalStore(
  withState(initialState),
  withMethods((store) => ({
    updateTodo(todoId: number, isCompleted: boolean) {
      patchState(store, (state) => {
        const todoIndex = state.todos.findIndex((todo) => todo.id === todoId);
        state.todos[todoIndex].completed = isCompleted;
        return { todos: [...state.todos] };
      });
    }))
);

Using withHooks in signalStore

What is withHooks?

  • A utility in signalStore that enables lifecycle hooks for the store.
  • Hooks let you execute code during specific lifecycle stages of the store.
  • Examples include initialization, cleanup, and side effects.

Why Use withHooks?

  1. Encapsulated Logic:

    • Centralize store-related lifecycle logic (e.g., API calls, event listeners).
  2. Resource Management:

    • Clean up resources (like subscriptions) when the store is destroyed.
  3. Declarative Lifecycle Handling:

    • Handle side effects directly within the store.

Practical Use Cases

  • Data Fetching:

    • Fetch API data when the store is initialized.
  • Subscriptions:

    • Start and clean up subscriptions to external observables.
  • Resource Cleanup:

    • Remove event listeners, timers, or observables when the store is destroyed.

 How Does withHooks Work?

export const UserStore = signalStore(
  withState(initialState),
  withHooks({
    onInit(store) {
      console.log('onInit called');
      const initialValue = localStorage.getItem(TodoStorageKey);
      if (initialValue) {
        patchState(store, () => ({
          todos: (JSON.parse(initialValue) as TodoI[]) || [],
        }));
      }
      patchState(store, () => ({ isLoading: true }));
      timer(2000).subscribe(() => {
        patchState(store, () => ({ isLoading: false }));
      });
    },
  })
);

 How Does withComputed Work?

export const UserStore = signalStore(
  withState(initialState),
  withHooks(({ onInit, onDestroy, update }) => {
    // onDestroy: Cleanup resources
    onDestroy(() => {
      console.log('UserStore destroyed');
    });
  })
);

Persisting State with Local Storage and Using Effects in signalStore

Why Persist State?

  1. Enhanced User Experience:

    • Preserve data across page reloads or sessions.
  2. Reduce Redundant Fetches:

    • Avoid re-fetching data if it hasn’t changed.
  3. Enable Offline Functionality:

    • Keep state accessible even when offline.

Practical Use Cases

  • User Preferences:

    • Store settings like theme, language, or layout preferences.
  • Shopping Carts:

    • Retain items added to the cart without needing a backend call.
  • Form Drafts:

    • Preserve form data during page transitions or reloads.

 How Does Persisting the store State?

export const TodoStore = signalStore(
  withState(initialState),
  withHooks({
    onInit: (store) => {
      // Load state from localStorage during initialization
      const storedTodos = localStorage.getItem('todos');
      if (storedTodos) {
        store.todos.set({ todos: JSON.parse(storedTodos) });
      }

      // Persist state to localStorage whenever it changes
      effect(() => {
        const currentState = getState(store);
        localStorage.setItem('todos', JSON.stringify(currentState.todos));
      });
    },
  })
);

Let's practice

THANK YOU

Contact me via email or Discord if you have any questions.

Angular training - NgRx

By Youcef Madadi

Angular training - NgRx

  • 227