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
Action stream management and type-safe action filtering capabilities for NgRx Effects, enabling precise control over which actions trigger specific effects.
Injectable service that provides access to the application's action stream, extending RxJS Observable with custom operators.
/**
* Injectable service providing access to all dispatched actions
* Extends Observable<Action> with additional NgRx-specific functionality
*/
class Actions<V = Action> extends Observable<V> {
/**
* Custom lift method for applying operators with NgRx context
* @param operator - RxJS operator to apply
* @returns Observable with applied operator
*/
lift<R>(operator?: Operator<V, R>): Observable<R>;
}Usage Examples:
import { Injectable } from "@angular/core";
import { Actions } from "@ngrx/effects";
@Injectable()
export class MyEffects {
constructor(private actions$: Actions) {
// actions$ is an Observable<Action> containing all dispatched actions
this.actions$.subscribe(action => {
console.log('Action dispatched:', action);
});
}
}
// In functional effects
export const myEffect = createEffect(
(actions$ = inject(Actions)) =>
actions$.pipe(
// Process all actions
tap(action => console.log('All actions:', action.type))
),
{ functional: true, dispatch: false }
);RxJS operator for filtering actions by type with full type safety and automatic type narrowing.
/**
* Filters actions by string type with type narrowing
* @param allowedTypes - Action type strings to filter by
* @returns OperatorFunction that filters and narrows action types
*/
function ofType<E extends Extract<A, { type: T }>, A extends Action = Action, T extends string = A['type']>(
...allowedTypes: [T, ...T[]]
): OperatorFunction<A, E>;
/**
* Filters actions by ActionCreator with automatic type inference
* @param allowedTypes - ActionCreator instances to filter by
* @returns OperatorFunction that filters to specific action types
*/
function ofType<AC extends ActionCreator<string, Creator>, U extends Action = Action>(
...allowedTypes: [AC, ...AC[]]
): OperatorFunction<U, ReturnType<AC>>;Usage Examples:
import { ofType } from "@ngrx/effects";
import { createAction, props } from "@ngrx/store";
// Define actions
const loadBooks = createAction('[Book] Load Books');
const loadBooksSuccess = createAction(
'[Book] Load Books Success',
props<{ books: Book[] }>()
);
const loadBooksFailure = createAction(
'[Book] Load Books Failure',
props<{ error: string }>()
);
// Filter by ActionCreator (recommended)
loadBooks$ = createEffect(() =>
this.actions$.pipe(
ofType(loadBooks), // Type is automatically narrowed to loadBooks action
switchMap(() =>
this.bookService.getBooks().pipe(
map(books => loadBooksSuccess({ books })),
catchError(error => of(loadBooksFailure({ error: error.message })))
)
)
)
);
// Filter by multiple ActionCreators
bookActions$ = createEffect(() =>
this.actions$.pipe(
ofType(loadBooksSuccess, loadBooksFailure), // Union type
tap(action => {
if (action.type === loadBooksSuccess.type) {
console.log('Books loaded:', action.books); // Type-safe access
} else {
console.log('Load failed:', action.error); // Type-safe access
}
})
),
{ dispatch: false }
);
// Filter by string types (legacy approach)
legacyEffect$ = createEffect(() =>
this.actions$.pipe(
ofType('[User] Load User', '[User] Update User'),
// Actions are typed as Action with specified types
map(action => {
// Manual type checking required
if (action.type === '[User] Load User') {
// Handle load user
}
return someOtherAction();
})
)
);Conditional Action Processing:
conditionalEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(UserActions.updateUser),
filter(action => action.user.isActive), // Additional filtering
switchMap(action =>
this.userService.updateUser(action.user).pipe(
map(() => UserActions.updateUserSuccess())
)
)
)
);Multiple Action Types with Different Handling:
multiActionEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(
DataActions.loadData,
DataActions.refreshData,
DataActions.reloadData
),
switchMap(action => {
// Different logic based on action type
const force = action.type === DataActions.reloadData.type;
return this.dataService.getData({ force }).pipe(
map(data => DataActions.loadDataSuccess({ data })),
catchError(error => of(DataActions.loadDataFailure({ error })))
);
})
)
);Action Filtering with State Dependency:
stateAwareEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(AppActions.someAction),
withLatestFrom(this.store.select(selectCurrentUser)),
filter(([action, user]) => user != null), // Only process if user exists
map(([action, user]) => AppActions.processWithUser({ action, user }))
)
);Debounced Action Processing:
debouncedSearchEffect$ = createEffect(() =>
this.actions$.pipe(
ofType(SearchActions.searchQuery),
debounceTime(300), // Wait 300ms between actions
distinctUntilChanged((prev, curr) => prev.query === curr.query),
switchMap(action =>
this.searchService.search(action.query).pipe(
map(results => SearchActions.searchSuccess({ results }))
)
)
)
);interface Action {
type: string;
}
interface ActionCreator<T extends string = string, C extends CreatorFunction<any> = CreatorFunction<any>> {
readonly type: T;
(...args: Parameters<C>): Action & ReturnType<C>;
}
type CreatorFunction<P> = (...args: any[]) => P;
interface Operator<T, R> {
(source: Observable<T>): Observable<R>;
}
type OperatorFunction<T, R> = (source: Observable<T>) => Observable<R>;