10th June 2024

State Management in Angular: NgRx and Alternative Approaches

State-Management-Image-VS-Online-img

State management is a critical aspect of building robust, scalable, and maintainable Angular applications. It involves maintaining the state or data of your application in a predictable and consistent way. This article will explore state management in Angular using NgRx and alternative approaches without NgRx.

1. Introduction to State Management

State management refers to the way an application manages its data, including user input, server responses, and UI state. Proper state management ensures data consistency, improves maintainability, and simplifies debugging. Angular provides several ways to manage state, ranging from simple service-based solutions to more complex state management libraries like NgRx.

2. State Management with NgRx

NgRx is a popular state management library for Angular that follows the Redux pattern. It provides a robust and scalable way to manage application state.

Installing NgRx

To get started with NgRx, you need to install the necessary packages:

                                
                                    
    npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools
  
                                
                            
Core Concepts of NgRx

NgRx consists of several core concepts:

  • Actions: Events that describe state changes.
  • Reducers: Functions that handle actions and determine how the state should change.
  • Selectors: Functions that extract specific pieces of state data.
  • Effects: Middleware for handling side effects, such as API calls.

Example Implementation

1. Define Actions

Actions represent events that change the state. Define actions using createAction:

                                
                                    
  import { createAction, props } from '@ngrx/store';
  export const loadItems = createAction('[Items] Load Items');
  export const loadItemsSuccess = createAction('[Items] Load Items Success', props<{ items: any[] }>());
  export const loadItemsFailure = createAction('[Items] Load Items Failure', props<{ error: any }>());
 
                                
                            
2. Define Reducers

Reducers specify how the state changes in response to actions. Use createReducer and on to define reducers:

                                
                                    
    import { createReducer, on } from '@ngrx/store';
    import { loadItems, loadItemsSuccess, loadItemsFailure } from './items.actions';
    
    export interface ItemsState {
      items: any[];
      loading: boolean;
      error: any;
    }
    
    export const initialState: ItemsState = {
      items: [],
      loading: false,
      error: null,
    };
    
    export const itemsReducer = createReducer(
      initialState,
      on(loadItems, state => ({ ...state, loading: true })),
      on(loadItemsSuccess, (state, { items }) => ({ ...state, items, loading: false })),
      on(loadItemsFailure, (state, { error }) => ({ ...state, error, loading: false })),
    );
 
                                
                            
3. Define Selectors

Selectors are used to retrieve specific pieces of state:

                                
                                    
  import { createFeatureSelector, createSelector } from '@ngrx/store';
  import { ItemsState } from './items.reducer';
  
  export const selectItemsState = createFeatureSelector<ItemsState>('items');
  
  export const selectAllItems = createSelector(selectItemsState, (state: ItemsState) => state.items);
  export const selectItemsLoading = createSelector(selectItemsState, (state: ItemsState) => state.loading);
  export const selectItemsError = createSelector(selectItemsState, (state: ItemsState) => state.error);
 
                                
                            
4. Set Up Store in App Module

Configure the NgRx store in your AppModule:

                                
                                    
    import { NgModule } from '@angular/core';
    import { StoreModule } from '@ngrx/store';
    import { EffectsModule } from '@ngrx/effects';
    import { itemsReducer } from './items.reducer';
    import { ItemsEffects } from './items.effects';
    
    @NgModule({
      imports: [
        StoreModule.forRoot({ items: itemsReducer }),
        EffectsModule.forRoot([ItemsEffects]),
      ],
    })
    export class AppModule {}
 
                                
                            
5. Use Store in Components

Dispatch actions and select state in your components:

                                
                                    
  import { Component, OnInit } from '@angular/core';
  import { Store } from '@ngrx/store';
  import { loadItems } from './items.actions';
  import { selectAllItems, selectItemsLoading, selectItemsError } from './items.selectors';
  import { Observable } from 'rxjs';
  
  @Component({
    selector: 'app-items',
    template: '
  <div *ngIf="loading$ | async">Loading...</div>
  <ul>
  <li *ngFor="let item of items$ | async">{{ item }}</li>
  </ul>
  <div *ngIf="error$ | async">{{ error$ | async }}</div>
    ',
  })
  export class ItemsComponent implements OnInit {
    items$: Observable<any[]>;
    loading$: Observable<boolean>;
    error$: Observable<any>;
  
    constructor(private store: Store) {
      this.items$ = this.store.select(selectAllItems);
      this.loading$ = this.store.select(selectItemsLoading);
      this.error$ = this.store.select(selectItemsError);
    }
  
    ngOnInit() {
      this.store.dispatch(loadItems());
    }
  }
 
                                
                            
6. Define Effects (Optional)

Use effects to handle side effects like API calls:

                                
                                    
  import { Injectable } from '@angular/core';
  import { Actions, createEffect, ofType } from '@ngrx/effects';
  import { of } from 'rxjs';
  import { catchError, map, mergeMap } from 'rxjs/operators';
  import { loadItems, loadItemsSuccess, loadItemsFailure } from './items.actions';
  import { ItemsService } from './items.service';
  
  @Injectable()
  export class ItemsEffects {
    loadItems$ = createEffect(() =>
      this.actions$.pipe(
        ofType(loadItems),
        mergeMap(() =>
          this.itemsService.getAll().pipe(
            map(items => loadItemsSuccess({ items })),
            catchError(error => of(loadItemsFailure({ error })))
          )
        )
      )
    );
  
    constructor(private actions$: Actions, private itemsService: ItemsService) {}
  }
 
                                
                            

3. Alternative State Management Approaches

While NgRx is powerful, it might be overkill for smaller applications. Here are some alternative state management approaches without using NgRx:

Service-Based State Management

Using services to manage state is a simple and effective approach for smaller applications.

Example Implementation
1. Create a Service:
                                
                                    
  import { Injectable } from '@angular/core';
 
  @Injectable({
    providedIn: 'root',
  })
  export class StateService {
    private _data: any;
  
    get data() {
      return this._data;
    }
  
    set data(value: any) {
      this._data = value;
    }
  }
 
                                
                            
2. Use the Service in Components:
                                
                                    
  import { Component, OnInit } from '@angular/core';
  import { StateService } from './state.service';
  
  @Component({
    selector: 'app-my-component',
    template: '<div>{{ stateService.data }}</div>',
  })
  export class MyComponent implements OnInit {
    constructor(private stateService: StateService) {}
  
    ngOnInit() {
      this.stateService.data = 'Hello, World!';
    }
  }
 
                                
                            

BehaviorSubject and Observables

Using BehaviorSubject and Observable for a more reactive state management approach.

Example Implementation
1. Create a Service with BehaviorSubject:
                                
                                    
  import { Injectable } from '@angular/core';
  import { BehaviorSubject, Observable } from 'rxjs';
  
  @Injectable({
    providedIn: 'root',
  })
  export class StateService {
    private _dataSubject: BehaviorSubject<any> = new BehaviorSubject(null);
  
    get data$(): Observable<any> {
      return this._dataSubject.asObservable();
    }
  
    setData(value: any) {
      this._dataSubject.next(value);
    }
  }
 
                                
                            
2. Subscribe to the Observable in a Component:
                                
                                    
  import { Component, OnInit } from '@angular/core';
  import { StateService } from './state.service';
  
  @Component({
    selector: 'app-my-component',
    template: '<div>{{ data | async }}</div>',
  })
  export class MyComponent implements OnInit {
    data: any;
  
    constructor(private stateService: StateService) {}
  
    ngOnInit() {
      this.stateService.data$.subscribe((value) => {
        this.data = value;
      });
  
      this.stateService.setData('Hello, Observable!');
    }
  }
 
                                
                            

4. Conclusion

State management is crucial for building scalable and maintainable Angular applications. While NgRx provides a powerful solution for larger applications, simpler approaches like service-based state management or using BehaviorSubject and Observable can be more suitable for smaller applications. Understanding the needs of your application will help you choose the right state management approach.

Let's develop your ideas into reality