Side effect model for @ngrx/store that manages asynchronous operations and external interactions in Angular applications using RxJS
—
Pending
Does it follow best practices?
Impact
Pending
No eval scenarios have been run
Pending
The risk profile of this skill
Lifecycle hooks, error handling, and advanced effect management capabilities for sophisticated NgRx Effects usage patterns and custom effect behaviors.
Interfaces for controlling effect registration, execution, and initialization with fine-grained control over effect behavior.
/**
* Hook for providing unique identifiers for effect instances
* Useful for debugging and effect instance tracking
*/
interface OnIdentifyEffects {
/**
* Provide unique identifier for this effect instance
* @returns Unique string identifier
*/
ngrxOnIdentifyEffects(): string;
}
/**
* Hook for controlling effect lifecycle and execution
* Allows custom handling of resolved effects before they run
*/
interface OnRunEffects {
/**
* Control effect execution with custom logic
* @param resolvedEffects$ - Stream of resolved effect notifications
* @returns Modified stream of effect notifications
*/
ngrxOnRunEffects(resolvedEffects$: Observable<EffectNotification>): Observable<EffectNotification>;
}
/**
* Hook for dispatching custom actions after effect registration
* Useful for initialization actions or setup logic
*/
interface OnInitEffects {
/**
* Return action to dispatch after effect registration
* @returns Action to dispatch during effect initialization
*/
ngrxOnInitEffects(): Action;
}Usage Examples:
import { Injectable } from "@angular/core";
import { Actions, createEffect, ofType, OnIdentifyEffects, OnRunEffects, OnInitEffects } from "@ngrx/effects";
@Injectable()
export class AdvancedEffects implements OnIdentifyEffects, OnRunEffects, OnInitEffects {
constructor(private actions$: Actions) {}
// Provide unique identifier for debugging
ngrxOnIdentifyEffects(): string {
return 'AdvancedEffects-' + Math.random().toString(36).substr(2, 9);
}
// Control effect execution with custom logic
ngrxOnRunEffects(resolvedEffects$: Observable<EffectNotification>): Observable<EffectNotification> {
return resolvedEffects$.pipe(
// Add custom logging
tap(notification => {
console.log(`Effect ${notification.propertyName} from ${notification.sourceName} executed`);
}),
// Add delay for testing or rate limiting
delay(100),
// Filter effects based on conditions
filter(notification => {
// Only run certain effects in production
if (environment.production && notification.propertyName === 'debugEffect$') {
return false;
}
return true;
})
);
}
// Dispatch initialization action
ngrxOnInitEffects(): Action {
return AppActions.effectsInitialized({
effectsClass: 'AdvancedEffects',
timestamp: Date.now()
});
}
// Regular effects
loadData$ = createEffect(() =>
this.actions$.pipe(
ofType(AppActions.loadData),
switchMap(() =>
this.dataService.loadData().pipe(
map(data => AppActions.loadDataSuccess({ data }))
)
)
)
);
debugEffect$ = createEffect(() =>
this.actions$.pipe(
tap(action => console.log('Debug action:', action))
),
{ dispatch: false }
);
}
// Example with initialization effect
@Injectable()
export class AuthEffects implements OnInitEffects {
constructor(private actions$: Actions, private authService: AuthService) {}
ngrxOnInitEffects(): Action {
// Check authentication status on effect initialization
const isAuthenticated = this.authService.isAuthenticated();
return isAuthenticated
? AuthActions.checkAuthSuccess()
: AuthActions.checkAuthFailure();
}
// Auth-related effects...
}Comprehensive error handling system with customizable error handlers and retry mechanisms.
/**
* Type definition for custom effects error handlers
* @param observable$ - Observable that may emit errors
* @param errorHandler - Angular ErrorHandler service
* @returns Observable with error handling applied
*/
type EffectsErrorHandler = <T extends Action>(
observable$: Observable<T>,
errorHandler: ErrorHandler
) => Observable<T>;
/**
* Default error handling implementation with retry logic
* @param observable$ - Observable to apply error handling to
* @param errorHandler - Angular ErrorHandler service
* @param retryAttemptLeft - Number of retry attempts remaining (default: 10)
* @returns Observable with default error handling and retry logic
*/
function defaultEffectsErrorHandler<T extends Action>(
observable$: Observable<T>,
errorHandler: ErrorHandler,
retryAttemptLeft: number = 10
): Observable<T>;
/** Injection token for custom error handlers */
const EFFECTS_ERROR_HANDLER: InjectionToken<EffectsErrorHandler>;Usage Examples:
import { Injectable, ErrorHandler } from "@angular/core";
import { EFFECTS_ERROR_HANDLER, defaultEffectsErrorHandler } from "@ngrx/effects";
// Custom error handler implementation
export const customEffectsErrorHandler: EffectsErrorHandler = <T extends Action>(
observable$: Observable<T>,
errorHandler: ErrorHandler
) => {
return observable$.pipe(
catchError((error, caught) => {
// Custom logging
console.error('Effect error:', error);
// Send to error reporting service
errorReportingService.reportError(error);
// Custom retry logic based on error type
if (error.status === 503) {
// Retry server unavailable errors with exponential backoff
return caught.pipe(delay(2000));
} else if (error.status >= 400 && error.status < 500) {
// Don't retry client errors
errorHandler.handleError(error);
return EMPTY;
} else {
// Use default handling for other errors
return defaultEffectsErrorHandler(caught, errorHandler, 3);
}
})
);
};
// Register custom error handler
bootstrapApplication(AppComponent, {
providers: [
provideStore({}),
provideEffects(AppEffects),
{
provide: EFFECTS_ERROR_HANDLER,
useValue: customEffectsErrorHandler
}
]
});
// Effect with custom error handling disabled
@Injectable()
export class CustomErrorEffects {
customHandlingEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(SomeActions.riskyAction),
switchMap(action =>
this.riskyService.performOperation(action.data).pipe(
map(result => SomeActions.success({ result })),
catchError(error => {
// Custom error handling logic
if (error.code === 'BUSINESS_ERROR') {
return of(SomeActions.businessError({ error: error.message }));
} else {
// Log and continue
console.error('Unexpected error:', error);
return EMPTY;
}
})
)
)
),
{ useEffectsErrorHandler: false } // Disable automatic error handling
);
}
// Error handler with different strategies per effect
export class ConditionalErrorHandler implements EffectsErrorHandler {
constructor(private notificationService: NotificationService) {}
<T extends Action>(
observable$: Observable<T>,
errorHandler: ErrorHandler
): Observable<T> {
return observable$.pipe(
catchError((error, caught) => {
// Different handling based on error context
if (error.effectName?.includes('critical')) {
// Critical effects - show user notification and don't retry
this.notificationService.showError('Critical operation failed');
errorHandler.handleError(error);
return EMPTY;
} else if (error.effectName?.includes('background')) {
// Background effects - silent retry
return caught.pipe(delay(5000));
} else {
// Default handling
return defaultEffectsErrorHandler(caught, errorHandler);
}
})
);
}
}Internal notification system for effect execution and monitoring.
/**
* Notification wrapper for effect outputs and metadata
*/
class EffectNotification {
/** The effect observable */
effect: Observable<any>;
/** Property name of the effect in the source class */
propertyName: PropertyKey;
/** Name of the source class containing the effect */
sourceName: string;
/** Instance of the source class */
sourceInstance: any;
/** RxJS notification containing the effect result */
notification: Notification<Action | null>;
}Constants and functions for effects system initialization.
/** Action type for effects initialization */
const ROOT_EFFECTS_INIT: "@ngrx/effects/init";
/**
* Action creator for effects initialization
* @returns Action indicating effects system initialization
*/
function rootEffectsInit(): Action;Usage Examples:
// Listen for effects initialization
@Injectable()
export class AppEffects {
effectsInit$ = createEffect(() =>
this.actions$.pipe(
ofType(ROOT_EFFECTS_INIT),
map(() => AppActions.systemInitialized())
)
);
}
// Custom initialization handling
@Injectable()
export class InitializationEffects {
handleInit$ = createEffect(() =>
this.actions$.pipe(
ofType(ROOT_EFFECTS_INIT),
withLatestFrom(this.store.select(selectAppConfig)),
switchMap(([action, config]) => [
AppActions.configLoaded({ config }),
AppActions.startBackgroundTasks(),
...(config.enableAnalytics ? [AnalyticsActions.initialize()] : [])
])
)
);
}Effect with State Machine:
@Injectable()
export class StateMachineEffects implements OnRunEffects {
private state = 'idle';
ngrxOnRunEffects(resolvedEffects$: Observable<EffectNotification>): Observable<EffectNotification> {
return resolvedEffects$.pipe(
tap(() => {
// Update internal state machine
this.updateState();
}),
filter(() => this.canExecuteEffects())
);
}
private updateState() {
// State machine logic
switch (this.state) {
case 'idle':
this.state = 'processing';
break;
case 'processing':
this.state = 'completed';
break;
}
}
private canExecuteEffects(): boolean {
return this.state !== 'blocked';
}
}Rate-Limited Effects:
@Injectable()
export class RateLimitedEffects implements OnRunEffects {
private lastExecution = 0;
private readonly rateLimitMs = 1000;
ngrxOnRunEffects(resolvedEffects$: Observable<EffectNotification>): Observable<EffectNotification> {
return resolvedEffects$.pipe(
filter(() => {
const now = Date.now();
if (now - this.lastExecution > this.rateLimitMs) {
this.lastExecution = now;
return true;
}
return false;
})
);
}
}