skip to content

In NgRx 22, how do you write and register a functional effect that saves a flight booking and maps the result to actions?

level: middleimportance: should knowfreq 55%

answer

  1. a function, not a class
  2. inject() in default parameters
  3. the second argument to createEffect
  4. filter with an action creator
  5. provideEffects at root or on a route

basics

~20 s

Export a const built with createEffect((actions$ = inject(Actions), api = inject(BookingApi)) => ..., { functional: true }), filter with ofType, map the API result to success or failure actions, and register it with provideEffects at root or in a route's providers.

solid answer

~40 s

A functional effect is a plain exported constant: `createEffect(source, { functional: true })`, where `source` takes its dependencies as default parameters such as `actions$ = inject(Actions)` and `bookingApi = inject(BookingApi)`, so a test can pass fakes instead. Inside, `actions$.pipe(ofType(BookingPageActions.saveClicked), ...)` narrows the stream to the save action with its props typed, a flattening operator calls `bookingApi.save(booking)`, and the inner pipe maps the response to `saveSucceeded` and catches errors into `of(saveFailed(...))`. You register it with `provideEffects(bookingEffects)`, passing the module namespace from `import * as bookingEffects`, either in `bootstrapApplication`'s providers or in a lazy route's `providers`. Class effects still work and are registered the same way; the `@Effect` decorator was removed in NgRx 15.

code

ts · 23 lines
ts
// booking.effects.ts
import { inject } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { catchError, concatMap, map, of } from 'rxjs';
import { BookingApi } from './booking-api.service';
import { BookingApiActions, BookingPageActions } from './booking.actions';

export const saveBooking = createEffect(
  (actions$ = inject(Actions), bookingApi = inject(BookingApi)) => {
    return actions$.pipe(
      ofType(BookingPageActions.saveClicked),
      concatMap(({ booking }) =>
        bookingApi.save(booking).pipe(
          map((saved) => BookingApiActions.saveSucceeded({ booking: saved })),
          catchError((error: { message: string }) =>
            of(BookingApiActions.saveFailed({ message: error.message }))
          )
        )
      )
    );
  },
  { functional: true }
);

go deeper

for a junior

Recall the pieces: createEffect with functional: true, dependencies from inject(), ofType on the Actions stream, and provideEffects to register the effect.

for a middle

Explain why dependencies go in default parameters, why ofType takes action creators, and where success and failure mapping sits so a failed save still leaves the effect running.

for a senior

Show judgment on placement: root versus route-level provideEffects for lazy features, class versus functional grouping, and mapResponse or map plus catchError for request effects.

for a principal

Set conventions a team can hold: one effects file per feature, functional effects registered on the feature route, and lint rules that keep effects mapping one action to one result.

## What a functional effect is An **effect** in NgRx is an observable that NgRx subscribes to on your behalf; each action it emits is dispatched back to the store. Since NgRx 15.2 an effect does not need a class: a **functional effect** is an exported constant produced by `createEffect` with `{ functional: true }` as its second argument. NgRx 22 supports both forms; the older `@Effect()` decorator was removed in NgRx 15, so `createEffect` is the only way to declare one. `createEffect(source, config)` takes: - **`source`**: a function returning the observable. For a functional effect NgRx calls it inside an injection context, so it may call `inject()`. - **`config`**: an `EffectConfig` whose defaults are `dispatch: true`, `functional: false` and `useEffectsErrorHandler: true`. Forgetting `functional: true` on an exported function means NgRx treats it like a class-property effect and calls the source immediately, outside any injection context, so `inject()` fails. ## Building the save-booking effect The booking screen dispatches `BookingPageActions.saveClicked({ booking })`. The effect should call the booking API once per click and answer with `BookingApiActions.saveSucceeded` or `BookingApiActions.saveFailed`. 1. **Declare dependencies as default parameters**: `(actions$ = inject(Actions), bookingApi = inject(BookingApi)) => ...`. The NgRx docs recommend this form because a unit test can call the function with a fake stream and a fake API without any injector. 2. **Filter** with `ofType(BookingPageActions.saveClicked)`. Passing the action creator, not a type string, narrows the TypeScript type so `booking` is typed downstream. `ofType` accepts several creators when one effect handles more than one action. 3. **Flatten** into the request with an operator chosen for the action's concurrency; a save usually uses `concatMap` so no save is cancelled or skipped. 4. **Map inside the inner pipe**: `map((saved) => BookingApiActions.saveSucceeded({ booking: saved }))`, then `catchError((error) => of(BookingApiActions.saveFailed({ message: error.message })))`. Putting both on the request observable, not after the flattening operator, keeps the effect alive after a failure. `@ngrx/operators` also ships `mapResponse({ next, error })`, which bundles that `map` plus `catchError` into one operator for exactly this case. ## Registering it | Where | How | When | |---|---|---| | Whole app | `provideEffects(bookingEffects)` in `bootstrapApplication` or `ApplicationConfig` providers, next to `provideStore()` | The effect is needed from startup | | Lazy feature | `provideEffects(bookingEffects)` in the feature route's `providers` | The booking area loads on demand | | NgModule apps | `EffectsModule.forRoot([...])` / `EffectsModule.forFeature([...])` | Older spelling; still shipped | `provideEffects` accepts effect classes and records of functional effects in any mix, so `import * as bookingEffects from './booking.effects'` registers every exported effect in that file. NgRx starts effects as soon as they are provided, and registering the same effects class again from another lazy feature does not make it run twice. ## What happens after registration - NgRx starts the effects as soon as `provideEffects` runs, so they hear every action dispatched from then on. The `Actions` stream keeps no history: an action dispatched before a lazy route's effects were registered is not replayed to them. - After the root effects are added, NgRx dispatches `ROOT_EFFECTS_INIT`; an effect can filter it with `ofType(ROOT_EFFECTS_INIT)` to run start-up work such as restoring a saved search. - The NgRx docs advise against using services from root-level effects together with services used through the `APP_INITIALIZER` token. ## The class form, for comparison ```ts @Injectable() export class BookingEffects { private actions$ = inject(Actions); private bookingApi = inject(BookingApi); saveBooking$ = createEffect(() => this.actions$.pipe(/* same pipeline */) ); } ``` The class form groups related effects and shares injected fields; the functional form removes the class, keeps each effect independently importable and makes the dependencies explicit parameters. Both are registered with `provideEffects(BookingEffects)` or alongside functional ones. ## Type safety you get for free - With the default `dispatch: true`, TypeScript requires the effect to emit actions. A value that is not an action is a compile error. - A common slip is `map(() => BookingApiActions.saveSucceeded)` without calling the creator. `createEffect`'s typing rejects it with the message *ActionCreator cannot be dispatched. Did you forget to call the action creator function?* - An effect that only navigates or logs must set `dispatch: false`, which also relaxes the return type. ## Checklist an interviewer listens for - `createEffect` plus `{ functional: true }` for a const effect; dependencies via `inject()` in parameters. - `ofType` with action creators, success and failure mapped inside the inner observable. - `provideEffects` at the root or on the lazy route that needs it. - No `@Effect()`, which no longer exists in current NgRx.

  • Why must inject() run in the default parameters or the synchronous body of a functional NgRx effect, not inside the pipe?
    NgRx calls the effect function once, inside an injection context, when `provideEffects` sets the effects up, and `inject()` works only during that synchronous call. Code inside `concatMap` or `map` runs later, as actions arrive, outside any injection context, so an `inject()` there throws. Resolve dependencies up front and close over them.
  • What does mapResponse from @ngrx/operators change in the save-booking effect?
    It replaces the inner `map` plus `catchError(... of(...))` pair with one operator: `mapResponse({ next: (saved) => saveSucceeded(...), error: (e) => saveFailed(...) })`. It still has to sit on the request observable inside the flattening operator, so a failure produces an action without ending the effect.

saying these in an interview costs you the question

  • Functional effects still need the @Effect() decorator on the exported function.
  • provideState registers effects for a lazy feature along with its reducer.
  • An exported createEffect function works without { functional: true }.
  • ofType needs the action's type string; action creators cannot be passed to it.
  • Registering an effects class in two lazy routes makes each action handled twice.