In NgRx, why does a flight-search page's search request belong in an effect rather than in the reducer or the component?
answer
- what a reducer is allowed to do
- state in, state out, synchronously
- the component only declares intent
- Actions stream fires after reducers
- the response comes back as an action
basics
~20 sAn NgRx reducer must be a pure, synchronous function from state and action to the next state, so it cannot wait for an API. An effect listens to dispatched actions, runs the request and dispatches a success or failure action back.
solid answer
~40 sA reducer receives the current state and an action and must return the next state synchronously, with no side effects; a request started there would answer after the reducer had already returned, and replaying actions in the DevTools would fire real calls. The component could call the service itself, but then every screen that searches repeats the loading and error handling, and the store never records what happened. An effect is the third place: it subscribes to the `Actions` stream, which emits each action *after* the reducers have handled it, filters with `ofType(FlightSearchActions.searchRequested)`, calls the flight API and maps the response to `searchSucceeded` or `searchFailed`. The reducers then store that result like any other action, so the component only dispatches intent and reads state through selectors.
code
ts · 25 linesimport { inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, map, of, switchMap } from 'rxjs';
import { FlightApi } from './flight-api.service';
import { FlightSearchActions } from './flight-search.actions';
// Reducer side (pure): on(FlightSearchActions.searchRequested,
// (state) => ({ ...state, loading: true }))
export const searchFlights = createEffect(
(actions$ = inject(Actions), flightApi = inject(FlightApi)) => {
return actions$.pipe(
ofType(FlightSearchActions.searchRequested),
switchMap(({ query }) =>
flightApi.search(query).pipe(
map((flights) => FlightSearchActions.searchSucceeded({ flights })),
catchError((error: { message: string }) =>
of(FlightSearchActions.searchFailed({ message: error.message }))
)
)
)
);
},
{ functional: true }
);go deeper
Recall the contract: a reducer takes state and an action and synchronously returns new state with no side effects, so requests live in effects that answer with new actions.
Walk the full loop: dispatch, reducer marks loading, Actions emits after reduction, effect filters with ofType, calls the API, maps to success or failure, reducer stores the result.
Explain what the split buys in production: replayable action logs, one owner for each API call, and effects testable by feeding actions in and asserting actions out.
Weigh what the indirection costs, an extra action pair and file per request, against one owner per API call, a replayable action log and effects that several features reuse.
## Three candidate homes for one request A flight-search page has a search box. When the traveller submits a query, something has to call the flight API, wait for the answer and put the flights into the store. In an NgRx application there are three places that code could live: the **component**, the **reducer**, or an **effect**. Interviewers ask this question to see whether you understand why NgRx gives side effects a place of their own. A **side effect** is anything a function does besides computing its return value: an HTTP request, a timer, a navigation, a write to `localStorage`, a log line. ## Why the reducer cannot do it A **reducer** in NgRx is a function built with `createReducer` and `on()` that receives the current state and one action and returns the next state. The store calls it synchronously for every dispatched action. That contract rules out a request: - **It must return now.** The reducer's return value *is* the new state. A request answers later, after the reducer has already returned, and nothing would receive the answer. - **It must be pure.** Given the same state and action it must produce the same result. That is what lets the Redux DevTools browser extension replay a recorded session; a reducer that fired requests would send them again on every replay. - **It must be cheap and predictable to test.** A pure reducer is tested with plain inputs and outputs, no HTTP mocking. The reducer still has a job in the search: it can react to `searchRequested` by setting `loading: true`, and to `searchSucceeded` by storing the flights. ## Why not the component The component could call the API directly and dispatch the result. It works, but: - every component that can start a search repeats the same loading, error and retry logic; - the request is tied to the component's lifetime and the knowledge of *how* flights are fetched leaks into the view; - the action log no longer shows what happened between the query and the results, which is much of the debugging value of a store. ## What an effect is An **effect** is a long-running observable that NgRx subscribes to for you. You declare it with `createEffect` and register it with `provideEffects`. The flow for one search is: 1. The component dispatches `FlightSearchActions.searchRequested({ query })`. 2. The store runs the reducers first; the search slice sets `loading: true`. 3. Only then does the injectable `Actions` stream emit the same action to every effect. 4. The search effect keeps it with `ofType(FlightSearchActions.searchRequested)`, calls the flight API, and maps the response to `searchSucceeded({ flights })`, or catches the error and returns `searchFailed({ error })`. 5. NgRx dispatches that new action; the reducer stores the flights or the error, and selectors hand the result to the view. | Step | Who | Pure? | |---|---|---| | Declare intent | Component, `store.dispatch` | yes, just an action | | Mark loading | Reducer, `on(searchRequested)` | yes | | Call the API | Effect, `createEffect` + `ofType` | no, that is its job | | Store the result | Reducer, `on(searchSucceeded)` | yes | | Show it | Selectors read by the view | yes | ## What each side gains The component's store code shrinks to dispatching an action and reading a selector. The reducer stays pure and replayable. The effect is the single place that knows about the flight API, so a second screen that dispatches the same action gets the same behaviour for free, and an effect can be tested by feeding it actions and checking the actions it emits. Effects are not limited to HTTP. The NgRx documentation shows effects built on other observable sources, such as DOM events, and effects that dispatch nothing at all, for example a navigation after a booking is saved. ## Misreadings to avoid - An effect does not replace the reducer: the success action it emits still needs an `on()` handler, or the flights never reach the state. - An NgRx effect is not Angular's `effect()` from `@angular/core`; that one reacts to signal changes, while an NgRx effect reacts to dispatched actions. - Effects see an action after the reducers, never before, so a state read inside the effect already reflects that action.
- Does an NgRx effect see the state before or after the reducers handled the same action?After. The store runs the reducers first and only then emits the action on the `Actions` stream that effects subscribe to. So if the reducer sets `loading: true` for `searchRequested`, a selector the search effect reads while handling that action already sees the flag.
- Can an NgRx effect start from something other than a dispatched action?Yes. An effect is any observable passed to `createEffect`; the NgRx docs show one built on `fromEvent(document, 'click')`. It usually still maps to actions, or is declared with `{ dispatch: false }` when it only performs the side effect.
A reducer is the ledger clerk who records each order slip the moment it arrives and never leaves the desk; an effect is the runner who reads each slip after it has been recorded, goes out to fetch the flights, and comes back with a new slip saying what happened.
saying these in an interview costs you the question
- A reducer can start the API call and update the state when the response arrives.
- Effects run before the reducers, so they only ever see the previous state.
- Once an effect handles the search, the success action needs no reducer handler.
- An NgRx effect is the same thing as Angular's effect() on signals.
- Effects are only for HTTP calls; timers and navigation belong in reducers.