How to use effects in NgRx
NgRx Effects handle side effects like HTTP requests, WebSocket connections, and localStorage operations outside of reducers, keeping state management pure. As the creator of CoreUI with 12 years of Angular development experience, I’ve built complex effect chains in enterprise applications that orchestrate multiple async operations, handle errors gracefully, and coordinate communication with external services for millions of users.
The most effective approach uses createEffect with RxJS operators for async operations.
Basic HTTP Effect
import { Injectable } from '@angular/core'
import { Actions, createEffect, ofType } from '@ngrx/effects'
import { HttpClient } from '@angular/common/http'
import { map, switchMap, catchError } from 'rxjs/operators'
import { of } from 'rxjs'
import * as UserActions from './user.actions'
@Injectable()
export class UserEffects {
loadUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUsers),
switchMap(() =>
this.http.get<User[]>('/api/users').pipe(
map(users => UserActions.loadUsersSuccess({ users })),
catchError(error =>
of(UserActions.loadUsersFailure({ error: error.message }))
)
)
)
)
)
constructor(
private actions$: Actions,
private http: HttpClient
) {}
}
Actions Definition
// user.actions.ts
import { createAction, props } from '@ngrx/store'
export const loadUsers = createAction('[User] Load Users')
export const loadUsersSuccess = createAction(
'[User] Load Users Success',
props<{ users: User[] }>()
)
export const loadUsersFailure = createAction(
'[User] Load Users Failure',
props<{ error: string }>()
)
export const loadUser = createAction(
'[User] Load User',
props<{ id: number }>()
)
export const loadUserSuccess = createAction(
'[User] Load User Success',
props<{ user: User }>()
)
Effect with Parameters
@Injectable()
export class UserEffects {
loadUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUser),
switchMap(({ id }) =>
this.http.get<User>(`/api/users/${id}`).pipe(
map(user => UserActions.loadUserSuccess({ user })),
catchError(error =>
of(UserActions.loadUsersFailure({ error: error.message }))
)
)
)
)
)
constructor(
private actions$: Actions,
private http: HttpClient
) {}
}
Dispatch Multiple Actions
@Injectable()
export class UserEffects {
createUser$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.createUser),
switchMap(({ user }) =>
this.http.post<User>('/api/users', user).pipe(
// Dispatch multiple actions
mergeMap(createdUser => [
UserActions.createUserSuccess({ user: createdUser }),
UserActions.showNotification({ message: 'User created!' })
]),
catchError(error =>
of(UserActions.createUserFailure({ error: error.message }))
)
)
)
)
)
constructor(
private actions$: Actions,
private http: HttpClient
) {}
}
Non-Dispatching Effects
@Injectable()
export class UserEffects {
// Effect that doesn't dispatch actions
logActions$ = createEffect(
() =>
this.actions$.pipe(
tap(action => console.log('Action dispatched:', action))
),
{ dispatch: false }
)
// Navigate after success
navigateAfterCreate$ = createEffect(
() =>
this.actions$.pipe(
ofType(UserActions.createUserSuccess),
tap(() => this.router.navigate(['/users']))
),
{ dispatch: false }
)
constructor(
private actions$: Actions,
private router: Router
) {}
}
Error Handling Strategies
@Injectable()
export class UserEffects {
// Retry on failure
loadUsersWithRetry$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.loadUsers),
switchMap(() =>
this.http.get<User[]>('/api/users').pipe(
retry(3), // Retry 3 times
map(users => UserActions.loadUsersSuccess({ users })),
catchError(error =>
of(UserActions.loadUsersFailure({ error: error.message }))
)
)
)
)
)
// Debounce requests
searchUsers$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.searchUsers),
debounceTime(300), // Wait 300ms after last action
switchMap(({ query }) =>
this.http.get<User[]>(`/api/users/search?q=${query}`).pipe(
map(users => UserActions.searchUsersSuccess({ users })),
catchError(error =>
of(UserActions.searchUsersFailure({ error: error.message }))
)
)
)
)
)
constructor(
private actions$: Actions,
private http: HttpClient
) {}
}
Chaining Effects
@Injectable()
export class OrderEffects {
// First effect: Create order
createOrder$ = createEffect(() =>
this.actions$.pipe(
ofType(OrderActions.createOrder),
switchMap(({ order }) =>
this.http.post<Order>('/api/orders', order).pipe(
map(createdOrder =>
OrderActions.createOrderSuccess({ order: createdOrder })
),
catchError(error =>
of(OrderActions.createOrderFailure({ error: error.message }))
)
)
)
)
)
// Second effect: Send confirmation after success
sendConfirmation$ = createEffect(() =>
this.actions$.pipe(
ofType(OrderActions.createOrderSuccess),
switchMap(({ order }) =>
this.http.post('/api/emails/confirmation', { orderId: order.id }).pipe(
map(() => OrderActions.sendConfirmationSuccess()),
catchError(error =>
of(OrderActions.sendConfirmationFailure({ error: error.message }))
)
)
)
)
)
constructor(
private actions$: Actions,
private http: HttpClient
) {}
}
LocalStorage Effect
@Injectable()
export class SettingsEffects {
saveSettings$ = createEffect(
() =>
this.actions$.pipe(
ofType(SettingsActions.updateSettings),
tap(({ settings }) => {
localStorage.setItem('settings', JSON.stringify(settings))
})
),
{ dispatch: false }
)
loadSettings$ = createEffect(() =>
this.actions$.pipe(
ofType(SettingsActions.loadSettings),
map(() => {
const settings = localStorage.getItem('settings')
return settings
? SettingsActions.loadSettingsSuccess({
settings: JSON.parse(settings)
})
: SettingsActions.loadSettingsFailure({
error: 'No settings found'
})
})
)
)
constructor(private actions$: Actions) {}
}
WebSocket Effect
@Injectable()
export class ChatEffects {
connectWebSocket$ = createEffect(() =>
this.actions$.pipe(
ofType(ChatActions.connect),
switchMap(() => {
const ws = new WebSocket('ws://localhost:3000')
return new Observable(observer => {
ws.onmessage = (event) => {
observer.next(
ChatActions.messageReceived({
message: JSON.parse(event.data)
})
)
}
ws.onerror = (error) => {
observer.error(ChatActions.connectionError({ error }))
}
return () => ws.close()
})
})
)
)
constructor(private actions$: Actions) {}
}
Register Effects
// app.module.ts
import { EffectsModule } from '@ngrx/effects'
import { UserEffects } from './store/user.effects'
import { OrderEffects } from './store/order.effects'
@NgModule({
imports: [
StoreModule.forRoot(reducers),
EffectsModule.forRoot([UserEffects, OrderEffects])
]
})
export class AppModule {}
// Feature module
@NgModule({
imports: [
StoreModule.forFeature('products', productReducer),
EffectsModule.forFeature([ProductEffects])
]
})
export class ProductModule {}
Best Practice Note
This is how we structure effects in all CoreUI Angular projects for clean side effect handling. NgRx Effects keep reducers pure by isolating async operations, HTTP requests, and external service interactions. Always use switchMap for cancellable requests (like search), mergeMap for parallel operations, concatMap for sequential operations, and exhaustMap for preventing duplicate requests. Handle errors gracefully with catchError and dispatch success/failure actions for every async operation.
For production applications, consider using CoreUI’s Angular Admin Template which includes pre-configured NgRx with effect patterns.
Related Articles
For complete NgRx implementation, check out how to use NgRx Store in Angular and how to use selectors in NgRx.



