State management in Angular applications becomes significantly more efficient and predictable with NgRx. However, behind the power of NgRx lies the reactive foundation built using RxJS. NgRx leverages Observables, Subjects, and various RxJS operators to handle state transitions, side effects, and asynchronous data streams.
This post explores the deep connection between RxJS and NgRx, explaining how to combine them effectively to create robust and maintainable Angular applications.
1. Understanding RxJS and Its Role in NgRx
RxJS (Reactive Extensions for JavaScript) is a library for reactive programming using Observables. It enables developers to handle asynchronous data streams with operators such as map
, filter
, switchMap
, mergeMap
, and catchError
.
NgRx, on the other hand, is a state management library built on top of RxJS. It uses reactive principles to manage global and local states predictably and immutably. Every part of NgRx—Actions, Reducers, Effects, and Selectors—depends on Observables from RxJS.
For example, NgRx Effects are nothing but Observable streams that react to Actions and trigger side effects (like HTTP requests).
2. The Relationship Between RxJS and NgRx
- RxJS provides the tools to reactively handle asynchronous data.
- NgRx provides a structured framework for managing state.
- Together, they create a unidirectional data flow system where:
- Actions dispatch changes.
- Reducers handle state mutations.
- Selectors read data.
- Effects handle asynchronous side effects using RxJS.
3. Setting Up NgRx and RxJS in Angular
To use NgRx with RxJS, first install the necessary packages:
npm install @ngrx/store @ngrx/effects @ngrx/entity @ngrx/store-devtools rxjs
Then, import them into your Angular module:
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { reducers } from './store/reducers';
import { AppEffects } from './store/effects/app.effects';
@NgModule({
imports: [
StoreModule.forRoot(reducers),
EffectsModule.forRoot([AppEffects])
]
})
export class AppModule {}
4. Defining Actions with RxJS in Mind
Actions in NgRx define what’s happening in the app. Each action is an event that triggers a state change.
import { createAction, props } from '@ngrx/store';
import { Data } from '../models/data.model';
export const loadData = createAction('[Data] Load Data');
export const loadDataSuccess = createAction('[Data] Load Data Success', props<{ data: Data[] }>());
export const loadDataFailure = createAction('[Data] Load Data Failure');
Here, loadData
initiates a process, and RxJS will help us handle asynchronous data fetching via Effects.
5. Creating Reducers
Reducers handle synchronous updates to the state based on incoming actions.
import { createReducer, on } from '@ngrx/store';
import { loadDataSuccess } from '../actions/data.actions';
import { Data } from '../models/data.model';
export interface DataState {
items: Data[];
}
export const initialState: DataState = {
items: []
};
export const dataReducer = createReducer(
initialState,
on(loadDataSuccess, (state, { data }) => ({ ...state, items: data }))
);
Reducers are pure functions that never handle asynchronous logic. That’s where RxJS and Effects come in.
6. Using RxJS in Effects
NgRx Effects are designed to handle side effects like API calls, file uploads, and user tracking. They rely heavily on RxJS operators.
Here’s an example combining RxJS with NgRx Effects:
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { ApiService } from '../../services/api.service';
import { loadData, loadDataSuccess, loadDataFailure } from '../actions/data.actions';
import { switchMap, map, catchError, of } from 'rxjs';
@Injectable()
export class DataEffects {
constructor(private actions$: Actions, private apiService: ApiService) {}
loadData$ = createEffect(() =>
this.actions$.pipe(
ofType(loadData),
switchMap(() =>
this.apiService.getData().pipe(
map(data => loadDataSuccess({ data })),
catchError(() => of(loadDataFailure()))
)
)
)
);
}
In this effect:
ofType(loadData)
filters for specific actions.switchMap
performs the API call reactively.map
transforms the result into a new action.catchError
ensures graceful error handling using RxJS Observables.
7. Combining RxJS Streams with Selectors
Selectors read state data reactively using Observables. You can also combine multiple selectors using RxJS operators.
import { createSelector } from '@ngrx/store';
import { DataState } from '../reducers/data.reducer';
export const selectDataState = (state: any) => state.data;
export const selectAllData = createSelector(
selectDataState,
(state: DataState) => state.items
);
Now, in a component, you can subscribe to this selector reactively:
this.data$ = this.store.select(selectAllData);
Or combine multiple streams:
this.combined$ = combineLatest([
this.store.select(selectAllData),
this.userService.currentUser$
]).pipe(
map(([data, user]) => data.filter(item => item.ownerId === user.id))
);
8. Merging NgRx with Custom RxJS Logic
You can integrate your own RxJS streams with NgRx to build reactive workflows. For instance:
import { merge, of } from 'rxjs';
import { map, startWith } from 'rxjs/operators';
const stream1$ = of('A', 'B', 'C');
const stream2$ = of(1, 2, 3);
merge(stream1$, stream2$)
.pipe(map(val => Value: ${val}
))
.subscribe(console.log);
Similarly, in NgRx, merging Observables from selectors and effects can help handle complex UI updates or chained API requests.
9. Using RxJS Operators in NgRx
Some of the most commonly used RxJS operators in NgRx Effects include:
Operator | Description |
---|---|
map | Transforms emitted data |
switchMap | Cancels previous stream and switches to a new one |
mergeMap | Flattens multiple streams simultaneously |
concatMap | Executes streams sequentially |
catchError | Handles errors gracefully |
withLatestFrom | Combines with another stream before emitting |
tap | For side effects like logging or debugging |
Example:
loadUserData$ = createEffect(() =>
this.actions$.pipe(
ofType(loadUserData),
withLatestFrom(this.store.select(selectUserId)),
switchMap(([_, userId]) =>
this.apiService.getUserData(userId).pipe(
map(data => loadUserDataSuccess({ data })),
catchError(() => of(loadUserDataFailure()))
)
)
)
);
10. Debugging RxJS and NgRx
For debugging reactive streams, you can use:
- NgRx Store DevTools (integrates with Redux DevTools).
- RxJS tap() for logging intermediate values.
Example:
.pipe(
tap(action => console.log('Action Triggered:', action)),
map(action => performSomeLogic(action))
)
11. Example Workflow Using RxJS and NgRx Together
Let’s combine all pieces:
- Dispatch
loadData
action. - Effect listens using
ofType(loadData)
. - Effect triggers API request using
switchMap
. - API result emits a
loadDataSuccess
action. - Reducer updates the state.
- Component listens via a selector and updates the UI.
This entire chain is reactive, powered by RxJS Observables and managed predictably by NgRx.
12. Best Practices for Combining RxJS and NgRx
- Always use pure reducers — no side effects inside reducers.
- Use Effects for all asynchronous operations.
- Prefer switchMap in most API calls to avoid race conditions.
- Combine selectors using RxJS operators like
combineLatest
orwithLatestFrom
. - Use
takeUntil
orasync
pipes to manage subscriptions automatically. - Keep your streams clean, predictable, and testable.
Leave a Reply