As the lead of an Angular hotel booking app that needs a store library, how would you weigh NGXS against NgRx, and what does each choice cost the team?
answer
- classes and DI versus functions
- where side effects live
- action lifecycle you can await
- explicitness versus boilerplate
- what the team already knows
basics
~20 sNGXS trades NgRx's pure reducers and separate effects for state classes whose DI-enabled @Action handlers update state and do async work in one place. That gives less ceremony and awaitable actions, at the cost of a less explicit, less pure design.
solid answer
~50 sI would compare the two on where logic lives. In NGXS a `@State` class is built by Angular DI, and its `@Action` handlers both write state through a `StateContext` and do async work, returning an Observable or Promise; each action has a lifecycle the caller can observe or await. NgRx's global Store splits the same flow into action creators, pure `createReducer` functions and separate `createEffect` classes, which is more files but makes every state transition a pure, individually testable function. So NGXS buys less ceremony and a gentler RxJS curve; NgRx buys explicitness and a strict separation that scales to many contributors. I would weigh the team's RxJS fluency, how much side-effect orchestration the app has, the testing style we want, and consistency with the organisation's other apps. Whichever we pick, I would pin conventions early: NGXS 22's standalone `provideStore` and `select()`, never the deprecated `@Select`.
go deeper
Know the headline difference: NGXS uses state classes whose @Action methods update state and call services, while NgRx uses reducer functions and separate effects.
Explain how async work, completion and selectors differ between the two, and name the NGXS 22 standalone APIs you would use.
Weigh concrete risks: race handling in parallel NGXS actions, testability of mixed handlers, and migrating deprecated @Select and NgxsModule code.
Own the call: tie it to team skills, side-effect complexity, testing strategy and organisational consistency, back it with a spike, and set conventions before the codebase sets them for you.
## The decision in one sentence Both libraries give an Angular app a single global store driven by actions and read through memoized selectors. The real difference is **where the code that changes state lives** and how much structure the library forces on you. This answer assumes the team has already decided a store library is warranted; whether it is at all is a separate question. ## How the two models differ | Concern | NGXS | NgRx global Store | |---|---|---| | Unit of state | A class decorated with `@State`, built by Angular DI | A reducer function built with `createReducer` and `on()` | | Actions | Classes with a `static readonly type` | Creators from `createAction` or `createActionGroup` | | State changes | `@Action` methods writing through `StateContext` | Pure reducer cases returning new state | | Async work | In the same `@Action` method, returning an Observable or Promise | In separate effects built with `createEffect` | | Completion | Each dispatch completes when its handlers finish; lifecycle statuses are observable | `store.dispatch(action)` returns nothing; effects dispatch follow-up actions | | Reading | `@Selector`, `createSelector`, `select()` returning a signal | `createSelector`, `store.select`, `selectSignal` | | DevTools | `withNgxsReduxDevtoolsPlugin()` | `provideStoreDevtools()` | NgRx also ships a lighter, signal-based store in the same family, the SignalStore. It is often the real alternative an NgRx team would weigh against NGXS for feature-level state, and it deserves its own evaluation rather than a footnote here. ## What NGXS buys, and what it costs **Gains:** - **Less ceremony.** One class holds the defaults, the handlers and often the selectors. There is no separate reducer and effects file per feature. - **Angular-native DI.** Handlers call injected services directly, so the store reads like the rest of the Angular code. - **Awaitable actions.** A component can `await` the result of the NGXS 22 `dispatch()` function, or listen with `ofActionSuccessful` or `ofActionErrored`, for flows such as "confirm the booking, then navigate". - **A gentler RxJS curve.** Handlers may return Promises and use `async`/`await`; state operators such as `patch` and `updateItem` cover immutable updates. **Costs:** - **Less purity.** State writes and side effects share a method, so a handler cannot be tested as a pure input-to-output function the way a reducer can. - **Implicit concurrency.** Async actions run in parallel by default; races need `cancelUncompleted`, `ctx.abortSignal` or explicit operators, and the team must know when to use each. - **API churn to manage.** NGXS 18 deprecated `@Select`, and `NgxsModule` is now deprecated in favour of `provideStore`; code written from older tutorials needs a migration plan. ## What NgRx buys, and what it costs - **Gains:** every transition is a pure function; side effects are isolated in effects, whose flattening operator is an explicit decision; the strict action, reducer, effect split scales to many contributors who need one obvious place for everything. - **Costs:** more files per feature, a steeper RxJS requirement for effects, and more indirection for a newcomer following one user interaction through the code. ## How I would decide as a lead 1. **Team skills.** A team fluent in RxJS absorbs NgRx effects easily; a team newer to RxJS ships faster with NGXS handlers. 2. **Shape of the side effects.** Complex orchestration across many actions favours explicit, isolated effects; mostly request-then-store flows favour NGXS handlers. 3. **Testing strategy.** If the team wants pure, table-driven tests of every transition, reducers fit better. 4. **Consistency.** Matching the organisation's other Angular apps lowers onboarding cost more than any API detail. 5. **Exit cost.** Components that only read through selectors and only write by dispatching actions are the cheapest to move to another store later, so enforce that boundary whichever library wins. 6. **A time-boxed spike.** Build one real flow, such as searching rooms and confirming a reservation, in both, and compare file count, test effort and how easily a newcomer follows it. ## Guardrails whichever way you go - Write down the current API as the house style: for NGXS 22, `provideStore`, `provideStates`, `select()`, `dispatch()` and `withNgxs*Plugin` functions, never `@Select` or `NgxsModule`. - Keep selectors in one layer so components never derive store data ad hoc. - Decide the concurrency rule per action kind before the first race appears in production.
- Your team already uses NGXS but has @Select and NgxsModule everywhere. How would you plan the migration?Treat it as incremental, since both are deprecated but still exported in NGXS 22. First switch the root to `provideStore` and route-level `provideStates`, and each `*PluginModule` to its `withNgxs*Plugin` function. Then replace `@Select` fields with `select()` signals or `inject(Store).select(...)`, converting any string or anonymous-function selectors into real `@Selector` methods first. A lint rule banning the old forms stops new usages.
- What would make you reconsider NGXS after a year in production?Signs that its informality stopped paying off: handlers that grew into long orchestration scripts mixing several services, recurring race bugs from parallel async actions, or tests that need heavy mocking because state writes and side effects share a method. At that point either impose stricter conventions, such as thin handlers delegating to services, or reassess the store choice for new features.
saying these in an interview costs you the question
- NGXS needs a separate effects layer for HTTP calls, just like NgRx.
- NGXS has no DevTools support, so time-travel debugging requires NgRx.
- The choice does not matter, since both libraries are the same API under different names.
- The library with fewer lines of code is always the better choice for a team.
- NGXS actions cannot be awaited or observed after they are dispatched.