Angular State Management Using NgRx Store

Sagarnath S
5 min readMay 11, 2024

--

Angular applications, especially those that grow in complexity, require efficient state management solutions. NgRx is a popular choice among Angular developers for managing application state in a predictable way. This blog will walk you through the basics of Angular state management using NgRx Store, covering beginner to advanced concepts and best practices.

Introduction to NgRx

NgRx is a library inspired by Redux, a predictable state container for JavaScript apps. It helps manage global and local states in Angular applications, making them easier to understand, test, and maintain. The core concept of NgRx revolves around actions, reducers, and selectors.

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. You send them to the store using store.dispatch().

Reducers

Reducers specify how the application’s state changes in response to actions sent to the store. Remember that actions only describe what happened, but don’t describe how the application’s state changes.

Selectors

Selectors are pure functions used to select, derive and compose pieces of state. They are used to extract data from the store, allowing components to subscribe to specific pieces of state.

To start using NgRx in your Angular project, you need to install the necessary packages:

npm install @ngrx/store @ngrx/effects @ngrx/store-devtools --save

Then, import the StoreModule into your AppModule:

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';

import { AppComponent } from './app.component';

@NgModule({
declarations: [
AppComponent
],
imports: [
BrowserModule,
StoreModule.forRoot({}) // Initialize the store
],
providers: [],
bootstrap: [AppComponent]
})
export class AppModule { }

Creating Your First Action and Reducer

Let’s create a simple action and reducer to manage a counter in our application.

Defining an Action: Create an action to increment the counter:

import { createAction, props } from '@ngrx/store';

export const increment = createAction(
'[Counter] Increment',
props<{ value: number }>()
);

Creating a Reducer: Now, let’s create a reducer to handle this action:

import { createReducer, on } from '@ngrx/store';
import { increment } from './counter.actions';

export interface CounterState {
count: number;
}

export const initialState: CounterState = {
count: 0
};

export const counterReducer = createReducer(
initialState,
on(increment, (state, { value }) => ({...state, count: state.count + value }))
);

Integrating the Reducer with the Store. Modify your AppModule to include the reducer:

import { StoreModule } from '@ngrx/store';
import { counterReducer } from './counter.reducer';

@NgModule({
imports: [
BrowserModule,
StoreModule.forRoot({ counter: counterReducer }) // Register the reducer
],
// other properties...
})
export class AppModule { }

Using the Store in Components

To use the store in your components, inject the Store service and use selectors to access the state:

import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { selectCount } from './counter.selectors';

@Component({
selector: 'app-root',
template: `
<div>
<h1>{{ count$ | async }}</h1>
<button (click)="increment()">Increment</button>
</div>
`,
})
export class AppComponent {
count$: Observable<number>;

constructor(private store: Store) {
this.count$ = store.select(selectCount);
}

increment() {
this.store.dispatch(increment({ value: 1 }));
}
}

Handling Side Effects with NgRx Effects

Effects are a powerful feature of NgRx that allow you to handle side effects, such as API calls, in a clean and organized manner. By moving side effects out of components and into effects, you keep your components focused on rendering and your effects focused on handling side effects.

Example: Making API Calls with Effects. First, define actions for loading data and handling the success or failure of the request:

// data.actions.ts
import { createAction, props } from '@ngrx/store';

export const loadData = createAction('[Data Page] Load Data');
export const loadDataSuccess = createAction('[Data Page] Load Data Success', props<{ data: any }>());
export const loadDataFailure = createAction('[Data Page] Load Data Failure', props<{ error: any }>());

Next, create an effect to handle the loadData action by making an HTTP request:

// data.effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { loadData, loadDataSuccess, loadDataFailure } from './data.actions';
import { of } from 'rxjs';
import { switchMap, catchError } from 'rxjs/operators';
import { HttpClient } from '@angular/common/http';

@Injectable()
export class DataEffects {
loadData$ = createEffect(() =>
this.actions$.pipe(
ofType(loadData),
switchMap(() =>
this.http.get('https://jsonplaceholder.typicode.com/posts').pipe(
map(data => loadDataSuccess({ payload: data })),
catchError(error => of(loadDataFailure({ payload: error })))
)
)
)
);

constructor(private actions$: Actions, private http: HttpClient) {}
}

Don’t forget to import EffectsModule in your AppModule:

// app.module.ts
import { EffectsModule } from '@ngrx/effects';
import { DataEffects } from './data.effects';

@NgModule({
imports: [
// other imports...
EffectsModule.forRoot([DataEffects])
]
})
export class AppModule { }

Using Selectors for Efficient State Access

Selectors are pure functions used to select, derive, and compose pieces of state. They are memoized by default, meaning NgRx will remember the result of a selector and only recompute it if the state has changed since the last time the selector was called.

Example: Memoizing Selectors , To ensure your selectors are memoized and thus efficient, avoid creating selectors that depend on external variables or that perform complex computations. Instead, rely on the state passed to the selector function:

// data.selectors.ts
import { createSelector } from '@ngrx/store';

export const selectData = (state: AppState) => state.data;

export const selectPosts = createSelector(
selectData,
(data: DataState) => data.posts
);

Performance Considerations

  • Minimize State Size: Keep your state as small as possible. Large states can lead to performance issues and make your application harder to reason about.
  • Use Selectors Wisely: Overuse of selectors can lead to performance degradation. Ensure selectors are pure and memoized to avoid unnecessary computations.
  • Optimize Effects: Effects should be optimized to avoid unnecessary subscriptions or side effects. Use RxJS operators like switchMap, concatMap, etc., wisely to manage subscriptions.

Thank you for reading if you have any doubts. drop the message in comments.

Follow me on Medium or LinkedIn to read more about Angular and TS!

--

--

Sagarnath S
Sagarnath S

Written by Sagarnath S

Software Development Engineer - I @CareStack || Web Developer || Angular || ASP.Net Core || C# || TypeScript || HTML || CSS

Responses (2)