NgRx Effects Architecture: Cancelling Obsolete Requests and Maintaining Data Boundaries
NgRx Effects centralize async side effects, but choosing the wrong flattening operator or boundary can corrupt state. Here is an architecture note on when to use them and how to verify cancellation.
26 Oct 2025, 19:08 UTC

The Problem: Side Effects Leaking Into Components and Reducers
When an Angular component calls an HTTP service directly and then dispatches actions based on the response, side-effect logic scatters across the codebase. Reducers risk becoming impure if they attempt to parse transport payloads or manage timers. The useful takeaway is that NgRx Effects give you a single, testable boundary for asynchronous work: components dispatch intent, Effects perform the side effect, and only normalized results re-enter the store.
Requirements for the Smallest Suitable Design
Before adding an Effect, confirm the design actually needs one. The smallest suitable design assumes four requirements:
- Unidirectional flow: components dispatch actions; they do not mutate state directly.
- Pure reducers: reducers only compute new state from the previous state and an action.
- Isolated side effects: API calls, timers, and browser APIs live outside reducers and components.
- Cancellation: rapid user input should not leave stale responses overwriting fresh data.
If the operation is a simple synchronous derivation—such as toggling a boolean flag—a reducer or selector is sufficient. Adding an Effect there introduces boilerplate without benefit.
Implementing the Effect Boundary
In NgRx version 8 and later, the createEffect factory replaces the older @Effect decorator. An Effect is an observable stream of actions. It listens for a specific action type, performs the side effect, and emits one or more new actions.
Consider a feature that loads a list of items. The component dispatches loadItems. An Effect intercepts it, calls the service, and emits either loadItemsSuccess or loadItemsFailure.
loadItems$ = createEffect(() =>
this.actions$.pipe(
ofType(loadItems),
switchMap(() =>
this.itemsService.getItems().pipe(
map(items => loadItemsSuccess({ items })),
catchError(error => of(loadItemsFailure({ error: error.message })))
)
)
)
);
Here, switchMap projects each source action to a new inner observable (the HTTP request). When a second loadItems arrives before the first completes, switchMap unsubscribes from the previous request. This prevents race conditions where an older, slower response could overwrite a newer one.
Trust and Data Boundaries
A common mistake is passing the raw HTTP response straight into the store. Transport shapes change; they include pagination envelopes, hypermedia links, or server-specific field names. The Effect should treat the API response as untrusted at the state boundary. Map it to a normalized, application-specific model before dispatching success. Reducers then operate on a stable contract, and components receive predictable data through selectors.
This boundary also simplifies testing. Because the Effect owns the mapping logic, unit tests can verify that a given API payload produces the expected normalized action without involving the reducer.
Operational Checks
Verification happens at three levels: the action stream, the cancellation behavior, and error containment.
Action sequence. With Redux DevTools open, dispatch the initiating action and confirm the Effect emits exactly one success or failure action. If the stream completes silently, the catchError placement is likely wrong.
Cancellation under load. Simulate a delayed response in a development service. Trigger the initiating action multiple times in rapid succession. The network panel should show earlier requests aborted or ignored, and only the final response should reach the reducer. If older responses still update state, the flattening operator is probably mergeMap rather than switchMap.
Error containment. Replace the service with a mock that throws. The Effect must emit the failure action and remain subscribed for future actions. If the stream terminates after the first error, subsequent dispatches will be ignored.
Failure Modes
Three failure modes appear frequently in Effect-heavy codebases.
Out-of-order updates with mergeMap. mergeMap subscribes to every inner observable immediately. If two requests complete out of order, the later-arriving older response can clobber the newer state. Use switchMap when only the latest result matters, or concatMap when order must be preserved at the cost of concurrency.
Circular action loops. An Effect that dispatches an action which triggers itself will spin indefinitely. Every Effect chain needs a distinct termination condition—typically a success or failure action that no longer matches the Effect's ofType filter.
Effect bloat. Placing trivial synchronous logic inside an Effect splits related behavior across actions, reducers, and effect files. If no asynchronous boundary exists, keep the logic in the reducer.
Conditions That Would Change the Design
The design above assumes a latest-wins policy for asynchronous reads. That assumption breaks under specific conditions:
- Batch or parallel writes. If the user can trigger multiple independent mutations that must all succeed—such as saving several form sections—
switchMapwill cancel unfinished requests. Switch tomergeMapor combine them explicitly and handle aggregation. - Ordered queues. When requests must execute sequentially,
concatMapenforces order but serializes network traffic. Evaluate whether the UX can tolerate the added latency. - Non-store side effects. Navigation, local storage, or third-party analytics often do not belong in the store. Handle them in services or route guards rather than Effects to avoid coupling global state to incidental concerns.
Limitations and a Practical Check
Effects add a layer of indirection. Debugging requires reading action logs rather than following a direct method call. They also assume the NgRx store is the appropriate home for the data in question; ephemeral component state may be better served by signals or local services.
A practical final check is a focused unit test. Provide MockStore or a test actions observable, inject a stub service, and assert that the Effect emits the expected success and failure actions for given inputs. If the test passes and Redux DevTools shows the correct sequence under rapid input, the boundary is behaving as designed.
Because NgRx APIs evolve, verify the exact createEffect signature and operator behavior against the version installed in your workspace before relying on this pattern in production.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.