NgRx Store is a state management library for Angular applications, inspired by Redux. It provides a single source of truth for the application state, making it easier to manage and debug complex state interactions. In this section, we will cover the following topics:

  1. Introduction to NgRx Store
  2. Setting Up NgRx Store
  3. Defining Actions
  4. Creating Reducers
  5. Using Selectors
  6. Dispatching Actions and Selecting State
  7. Practical Example
  8. Exercises

  1. Introduction to NgRx Store

NgRx Store is based on the principles of Redux, which include:

  • Single Source of Truth: The state of the entire application is stored in a single object tree within a store.
  • State is Read-Only: The only way to change the state is by dispatching actions.
  • Changes are Made with Pure Functions: Reducers are pure functions that take the current state and an action, and return a new state.

  1. Setting Up NgRx Store

To set up NgRx Store in your Angular application, follow these steps:

  1. Install NgRx Store:

    npm install @ngrx/store
    
  2. Add StoreModule to AppModule:

    import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { StoreModule } from '@ngrx/store';
    import { AppComponent } from './app.component';
    import { reducers } from './reducers';
    
    @NgModule({
      declarations: [AppComponent],
      imports: [
        BrowserModule,
        StoreModule.forRoot(reducers)
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule {}
    

  1. Defining Actions

Actions are payloads of information that send data from your application to your store. They are the only source of information for the store.

  1. Create an Action:
    import { createAction, props } from '@ngrx/store';
    
    export const increment = createAction('[Counter Component] Increment');
    export const decrement = createAction('[Counter Component] Decrement');
    export const reset = createAction('[Counter Component] Reset');
    

  1. Creating Reducers

Reducers specify how the application's state changes in response to actions sent to the store.

  1. Create a Reducer:
    import { createReducer, on } from '@ngrx/store';
    import { increment, decrement, reset } from './counter.actions';
    
    export const initialState = 0;
    
    const _counterReducer = createReducer(
      initialState,
      on(increment, state => state + 1),
      on(decrement, state => state - 1),
      on(reset, state => 0)
    );
    
    export function counterReducer(state, action) {
      return _counterReducer(state, action);
    }
    

  1. Using Selectors

Selectors are pure functions used for obtaining slices of store state.

  1. Create a Selector:
    import { createSelector, createFeatureSelector } from '@ngrx/store';
    
    export const selectCounter = createFeatureSelector<number>('counter');
    

  1. Dispatching Actions and Selecting State

To interact with the store, you can dispatch actions and select state in your components.

  1. Dispatching Actions:

    import { Component } from '@angular/core';
    import { Store } from '@ngrx/store';
    import { increment, decrement, reset } from './counter.actions';
    
    @Component({
      selector: 'app-counter',
      templateUrl: './counter.component.html'
    })
    export class CounterComponent {
      constructor(private store: Store<{ counter: number }>) {}
    
      increment() {
        this.store.dispatch(increment());
      }
    
      decrement() {
        this.store.dispatch(decrement());
      }
    
      reset() {
        this.store.dispatch(reset());
      }
    }
    
  2. Selecting State:

    import { Component } from '@angular/core';
    import { Store, select } from '@ngrx/store';
    import { Observable } from 'rxjs';
    import { selectCounter } from './counter.selectors';
    
    @Component({
      selector: 'app-counter',
      templateUrl: './counter.component.html'
    })
    export class CounterComponent {
      count$: Observable<number>;
    
      constructor(private store: Store<{ counter: number }>) {
        this.count$ = store.pipe(select(selectCounter));
      }
    }
    

  1. Practical Example

Let's create a simple counter application using NgRx Store.

  1. Define Actions:

    // counter.actions.ts
    import { createAction } from '@ngrx/store';
    
    export const increment = createAction('[Counter Component] Increment');
    export const decrement = createAction('[Counter Component] Decrement');
    export const reset = createAction('[Counter Component] Reset');
    
  2. Create Reducer:

    // counter.reducer.ts
    import { createReducer, on } from '@ngrx/store';
    import { increment, decrement, reset } from './counter.actions';
    
    export const initialState = 0;
    
    const _counterReducer = createReducer(
      initialState,
      on(increment, state => state + 1),
      on(decrement, state => state - 1),
      on(reset, state => 0)
    );
    
    export function counterReducer(state, action) {
      return _counterReducer(state, action);
    }
    
  3. Create Selector:

    // counter.selectors.ts
    import { createFeatureSelector } from '@ngrx/store';
    
    export const selectCounter = createFeatureSelector<number>('counter');
    
  4. Update AppModule:

    // app.module.ts
    import { NgModule } from '@angular/core';
    import { BrowserModule } from '@angular/platform-browser';
    import { StoreModule } from '@ngrx/store';
    import { AppComponent } from './app.component';
    import { counterReducer } from './counter.reducer';
    
    @NgModule({
      declarations: [AppComponent],
      imports: [
        BrowserModule,
        StoreModule.forRoot({ counter: counterReducer })
      ],
      providers: [],
      bootstrap: [AppComponent]
    })
    export class AppModule {}
    
  5. Create Counter Component:

    // counter.component.ts
    import { Component } from '@angular/core';
    import { Store, select } from '@ngrx/store';
    import { Observable } from 'rxjs';
    import { increment, decrement, reset } from './counter.actions';
    import { selectCounter } from './counter.selectors';
    
    @Component({
      selector: 'app-counter',
      templateUrl: './counter.component.html'
    })
    export class CounterComponent {
      count$: Observable<number>;
    
      constructor(private store: Store<{ counter: number }>) {
        this.count$ = store.pipe(select(selectCounter));
      }
    
      increment() {
        this.store.dispatch(increment());
      }
    
      decrement() {
        this.store.dispatch(decrement());
      }
    
      reset() {
        this.store.dispatch(reset());
      }
    }
    
  6. Counter Component Template:

    <!-- counter.component.html -->
    <div>
      <h1>Counter: {{ count$ | async }}</h1>
      <button (click)="increment()">Increment</button>
      <button (click)="decrement()">Decrement</button>
      <button (click)="reset()">Reset</button>
    </div>
    

  1. Exercises

Exercise 1: Add a New Action

Task: Add a new action to double the counter value.

  1. Define the Action:

    export const double = createAction('[Counter Component] Double');
    
  2. Update the Reducer:

    const _counterReducer = createReducer(
      initialState,
      on(increment, state => state + 1),
      on(decrement, state => state - 1),
      on(reset, state => 0),
      on(double, state => state * 2)
    );
    
  3. Update the Component:

    double() {
      this.store.dispatch(double());
    }
    
  4. Update the Template:

    <button (click)="double()">Double</button>
    

Exercise 2: Add a New Feature

Task: Add a new feature to track the number of times the counter has been incremented.

  1. Define the State:

    export interface AppState {
      counter: number;
      incrementCount: number;
    }
    
  2. Update the Reducer:

    export const initialState: AppState = {
      counter: 0,
      incrementCount: 0
    };
    
    const _counterReducer = createReducer(
      initialState,
      on(increment, state => ({
        ...state,
        counter: state.counter + 1,
        incrementCount: state.incrementCount + 1
      })),
      on(decrement, state => ({
        ...state,
        counter: state.counter - 1
      })),
      on(reset, state => ({
        ...state,
        counter: 0,
        incrementCount: 0
      }))
    );
    
    export function counterReducer(state, action) {
      return _counterReducer(state, action);
    }
    
  3. Update the Selector:

    export const selectIncrementCount = createSelector(
      selectCounter,
      (state: AppState) => state.incrementCount
    );
    
  4. Update the Component:

    incrementCount$: Observable<number>;
    
    constructor(private store: Store<AppState>) {
      this.count$ = store.pipe(select(selectCounter));
      this.incrementCount$ = store.pipe(select(selectIncrementCount));
    }
    
  5. Update the Template:

    <h2>Increment Count: {{ incrementCount$ | async }}</h2>
    

Conclusion

In this section, we covered the basics of NgRx Store, including setting it up, defining actions, creating reducers, using selectors, and dispatching actions. We also provided a practical example and exercises to reinforce the concepts. Understanding NgRx Store is crucial for managing state in large Angular applications, and it provides a robust and scalable solution for state management.

© Copyright 2024. All rights reserved