Overview
Effects provide a way to isolate side effects (API calls, logging, navigation, etc.) from components and facades. They listen to actions dispatched to the store and trigger side effects in response.
Rodando Passenger uses NgRx Effects selectively for cross-cutting concerns like notifications and analytics. Most side effects are handled directly in facades.
When to Use Effects
Use Effects for:
Cross-cutting concerns : Logging, analytics, notifications
Action chaining : Trigger multiple actions in response to one
External integrations : Third-party services, analytics platforms
Debouncing/throttling : Rate-limiting API calls
Don’t overuse Effects . For simple workflows, handle side effects in facades. Effects add complexity and should be reserved for scenarios where they provide clear value.
Notification Alert Effects
The primary use of Effects in Rodando Passenger is for displaying toast notifications in response to various events.
Effect Implementation
src/app/store/notification-alerts/notification-alert.effects.ts
import { inject } from '@angular/core' ;
import { Actions , createEffect , ofType } from '@ngrx/effects' ;
import { tap } from 'rxjs' ;
import { ToastController } from '@ionic/angular' ;
import * as NotificationActions from './notification-alert.actions' ;
export class NotificationAlertEffects {
private actions$ = inject ( Actions );
private toastController = inject ( ToastController );
showNotification$ = createEffect (
() =>
this . actions$ . pipe (
ofType ( NotificationActions . showNotificationAlert ),
tap ( async ( action ) => {
const toast = await this . toastController . create ({
message: action . message ,
duration: action . duration ?? 3000 ,
position: action . position ?? 'bottom' ,
color: action . color ?? 'primary' ,
buttons: action . dismissible
? [{ text: 'Dismiss' , role: 'cancel' }]
: undefined ,
});
await toast . present ();
})
),
{ dispatch: false }
);
}
Action Definition
src/app/store/notification-alerts/notification-alert.actions.ts
import { createAction , props } from '@ngrx/store' ;
export const showNotificationAlert = createAction (
'[Notification] Show Alert' ,
props <{
message : string ;
duration ?: number ;
position ?: 'top' | 'bottom' | 'middle' ;
color ?: 'primary' | 'secondary' | 'success' | 'warning' | 'danger' ;
dismissible ?: boolean ;
}>()
);
Usage in Facades
Facades dispatch notification actions:
src/app/store/auth/auth.facade.ts
import { Store } from '@ngrx/store' ;
import { showNotificationAlert } from '../notification-alerts/notification-alert.actions' ;
export class AuthFacade {
private store = inject ( Store );
login ( credentials : LoginPayload ) : Observable < User > {
return this . authService . login ( credentials ). pipe (
tap (() => {
this . store . dispatch (
showNotificationAlert ({
message: 'Welcome back!' ,
color: 'success' ,
duration: 2000 ,
})
);
}),
catchError (( err ) => {
this . store . dispatch (
showNotificationAlert ({
message: err . message ?? 'Login failed' ,
color: 'danger' ,
duration: 4000 ,
})
);
return throwError (() => err );
})
);
}
}
Effect Patterns
Non-Dispatching Effects
Effects that don’t dispatch new actions (like showing toasts) must set { dispatch: false }:
showNotification$ = createEffect (
() => this . actions$ . pipe (
ofType ( NotificationActions . showNotificationAlert ),
tap ( async ( action ) => {
// Side effect logic here
})
),
{ dispatch: false } // ⚠️ Required!
);
Action Chaining
Effects can dispatch multiple actions in response:
loginSuccess$ = createEffect (() =>
this . actions$ . pipe (
ofType ( AuthActions . loginSuccess ),
concatMap (( action ) => [
showNotificationAlert ({ message: 'Login successful' , color: 'success' }),
AnalyticsActions . trackEvent ({ event: 'login' , userId: action . user . id }),
NavigationActions . navigate ({ path: '/home' }),
])
)
);
Debouncing API Calls
Effects can debounce rapid user input:
searchDestination$ = createEffect (() =>
this . actions$ . pipe (
ofType ( TripActions . searchDestinationInput ),
debounceTime ( 300 ),
distinctUntilChanged (),
switchMap (( action ) =>
this . mapboxService . search ( action . query ). pipe (
map (( results ) => TripActions . searchDestinationSuccess ({ results })),
catchError (( error ) => of ( TripActions . searchDestinationFailure ({ error })))
)
)
)
);
Error Handling
Effects should handle errors gracefully:
loadTrips$ = createEffect (() =>
this . actions$ . pipe (
ofType ( TripActions . loadTrips ),
switchMap (() =>
this . tripsService . getTrips (). pipe (
map (( trips ) => TripActions . loadTripsSuccess ({ trips })),
catchError (( error : ApiError ) => {
// Log error, show notification, etc.
this . logger . error ( 'Failed to load trips' , error );
return of ( TripActions . loadTripsFailure ({ error }));
})
)
)
)
);
Registering Effects
Effects must be registered in the app configuration:
import { provideEffects } from '@ngrx/effects' ;
import { NotificationAlertEffects } from './store/notification-alerts/notification-alert.effects' ;
export const appConfig : ApplicationConfig = {
providers: [
provideStore (),
provideEffects ([ NotificationAlertEffects ]),
// ... other providers
],
};
Testing Effects
Effects can be tested using provideMockActions:
import { provideMockActions } from '@ngrx/effects/testing' ;
import { Observable , of } from 'rxjs' ;
describe ( 'NotificationAlertEffects' , () => {
let actions$ : Observable < any >;
let effects : NotificationAlertEffects ;
let toastController : jasmine . SpyObj < ToastController >;
beforeEach (() => {
actions$ = provideMockActions (() => of ());
toastController = jasmine . createSpyObj ( 'ToastController' , [ 'create' ]);
effects = new NotificationAlertEffects ( actions$ , toastController );
});
it ( 'should show a toast when showNotificationAlert is dispatched' , ( done ) => {
const mockToast = jasmine . createSpyObj ( 'Toast' , [ 'present' ]);
toastController . create . and . returnValue ( Promise . resolve ( mockToast ));
actions$ = of (
showNotificationAlert ({ message: 'Test message' , color: 'success' })
);
effects . showNotification$ . subscribe (() => {
expect ( toastController . create ). toHaveBeenCalledWith ({
message: 'Test message' ,
color: 'success' ,
duration: 3000 ,
position: 'bottom' ,
buttons: undefined ,
});
expect ( mockToast . present ). toHaveBeenCalled ();
done ();
});
});
});
Effects vs Facades
Use Effects When
Use Facades When
Cross-cutting concerns : Logging, analytics, notifications that span multiple features
Action chaining : Need to dispatch multiple actions in response to one
Debouncing/throttling : Rate-limiting user input or API calls
Global event handling : Browser events, WebSocket messages
Feature-specific logic : Business rules for a single feature
Simple workflows : Login, logout, CRUD operations
Direct component interaction : UI-driven workflows
Most use cases : Facades are simpler and easier to understand
Best Practices
Keep Effects Focused : Each effect should handle a single concern. Don’t create “god effects” that handle many action types.
Use switchMap for Cancellable Requests : If a user triggers the same action multiple times (e.g., search), use switchMap to cancel previous requests.
Avoid Infinite Loops : Be careful when dispatching actions from effects that listen to those same actions. Use guards or different action types.
Stores Signal-based state stores
Facades Business logic orchestration layer
State Management Overall state management architecture