Choosing Between NgRx Global Effects and ComponentStore for Async Logic
Decide between NgRx Global Effects and ComponentStore for async logic. Learn when to use global event streams versus localized state to avoid action pollution and improve performance.
09 Aug 2025, 07:21 UTC

The Dilemma: Global State vs. Localized Logic
When implementing asynchronous operations in an Angular application—such as API calls or timers—developers often struggle with where to place the logic. Placing every single interaction in the global NgRx Store leads to "action pollution," where the global action stream is cluttered with transient UI events (e.g., ToggleDropdown or LoadingSpinnerStart) that no other part of the application cares about.
The goal is to balance predictability (global state) with developer velocity (local state). The decision rests on whether the result of the asynchronous operation needs to be accessed by distant components or if it is purely for the current view.
Comparison of Side-Effect Strategies
| Feature | @ngrx/effects (Global) | @ngrx/component-store (Local) |
|---|---|---|
| Scope | Application-wide | Component subtree |
| Boilerplate | High (Action, Reducer, Effect) | Low (Service-based) |
| Lifecycle | Singleton (App lifetime) | Destroyed with component |
| DevTools | Full Redux DevTools integration | Limited/Optional integration |
| Best Use Case | Auth, User Profiles, Global Cache | Complex Forms, Local Data Tables |
Trade-offs and Decision Logic
When to use Global Effects
Global Effects are event-driven streams that listen for actions and trigger side effects. Use these when the data must persist across route changes or when multiple unrelated components must react to the same event. For example, an AuthLogout action should clear the user profile, reset the shopping cart, and redirect to login—all handled by separate global effects.
When to use ComponentStore
ComponentStore is a lightweight state management solution provided as an injectable service. It is ideal for "transient state"—data that is only useful while a specific component is mounted. Using ComponentStore prevents the global store from becoming a dumping ground for temporary UI flags, reducing the cognitive load when debugging the main application state.
The Risk of RxJS Operator Misuse
Regardless of the choice, the choice of RxJS flattening operator is critical. Using switchMap for a "Save" operation is dangerous because if the user clicks "Save" twice, the first request is cancelled, potentially leaving the backend in an inconsistent state. For write operations, concatMap (sequential) or exhaustMap (ignore new until finished) are the safer choices.
Implementation Example: Data Fetching
Consider a scenario where we fetch a list of items. Below is the implementation contrast.
Option A: Global Effect (The Standard Loop)
Run this in a dedicated .effects.ts file. Requires @ngrx/effects.
// Action definition
export const loadItems = createAction('[Items Page] Load');
export const loadItemsSuccess = createAction('[Items API] Success', props<{ items: Item[] }>());
// Effect implementation
loadItems$ = createEffect(() => this.actions$.pipe(
ofType(loadItems),
switchMap(() => this.itemsService.getAll().pipe(
map(items => loadItemsSuccess({ items })),
catchError(error => of(loadItemsFailure(error)))
))
));
Option B: ComponentStore (The Localized Approach)
Run this within a service that extends ComponentStore. Provide this service at the component level (providers: [ItemsLocalStore]) to ensure it is destroyed when the component is destroyed.
export class ItemsLocalStore extends ComponentStore<ItemsState> {
constructor(private itemsService: ItemsService) {
super({ items: [], loading: false });
}
readonly loadItems = this.effect((trigger$) => trigger$.pipe(
tap(() => this.patchState({ loading: true })),
switchMap(() => this.itemsService.getAll().pipe(
tap(items => this.patchState({ items, loading: false })),
catchError(err => of(err))
))
));
}
Verification and Validation
To verify the implementation, use the following checks:
- For Global Effects: Open Redux DevTools. Dispatch the action and verify that the
Loadaction is followed by aSuccessaction and that the state update is reflected in the global tree. - For ComponentStore: Navigate away from the component and back. If the state resets to the initial values, the store was correctly scoped to the component lifecycle. If the data persists, you have likely provided the store in
root, which defeats the purpose of a local store.
Rollback Procedure
If moving from ComponentStore back to Global Store: 1) Define the necessary Actions and Reducers. 2) Migrate the logic from the effect() method to a createEffect()` function. 3) Update the component to dispatch actions via the Store service instead of calling the local store method.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.