Angular State Management Using NgRx Store
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.