skip to content

In NGXS, how do you define a state slice with @State, change it from an @Action handler, and register it in a standalone Angular app?

level: juniorimportance: must knowfreq 38%

answer

  1. a class, not a reducer function
  2. name and defaults metadata
  3. action class with static type
  4. handler receives context, then action
  5. provideStore in the app providers

basics

~10 s

An NGXS state is a class decorated with @State({ name, defaults }) and @Injectable(); its @Action(ActionClass) methods receive a StateContext and the action and write through setState or patchState. provideStore([RoomsState]) registers it.

solid answer

~40 s

In NGXS a slice of state is a class. `@State<RoomsStateModel>({ name: 'rooms', defaults: { ... } })` declares the slice's unique name and initial value, and `@Injectable()` placed right after it lets Angular's DI construct the class, so it can inject services. An action is a plain class with a `static readonly type` string such as `'[Rooms Page] Select Room'`, carrying its payload as constructor fields. A method decorated with `@Action(SelectRoom)` runs whenever that action is dispatched; it receives a `StateContext` (`getState`, `setState`, `patchState`, `dispatch`) and the action instance, and writes the new state through the context rather than returning it. In an NGXS 22 standalone app you register root states with `provideStore([RoomsState])` and dispatch with the `dispatch(SelectRoom)` function or `inject(Store).dispatch(new SelectRoom(id))`. `NgxsModule.forRoot` still exists but is deprecated.

code

ts · 22 lines
ts
import { Injectable } from '@angular/core';
import { Action, State, StateContext } from '@ngxs/store';

export interface Room { id: string; number: string; status: 'free' | 'booked'; }
export interface RoomsStateModel { rooms: Room[]; selectedRoomId: string | null; }

export class SelectRoom {
  static readonly type = '[Rooms Page] Select Room';
  constructor(public roomId: string) {}
}

@State<RoomsStateModel>({
  name: 'rooms',
  defaults: { rooms: [], selectedRoomId: null }
})
@Injectable()
export class RoomsState {
  @Action(SelectRoom)
  selectRoom(ctx: StateContext<RoomsStateModel>, action: SelectRoom) {
    ctx.patchState({ selectedRoomId: action.roomId });
  }
}

go deeper

for a junior

Be able to write a @State class with name and defaults, an action class with a static type, and an @Action handler that calls patchState. Know that provideStore registers it.

for a middle

Explain what StateContext offers, why the handler writes through the context instead of returning state, and how provideStates adds route-level states under a root provideStore.

for a senior

Show that you know several states can answer one action, that state classes use DI, and that NgxsModule and @Select are deprecated leftovers to migrate away from.

for a principal

Frame the class-plus-decorator model as a design choice: fewer files and DI inside state, against the explicitness of pure reducer functions, and what that means for a team's conventions.

## The three pieces NGXS asks you to write NGXS is a state-management library for Angular that models each **slice** of application state as a **class** instead of a reducer function. Three pieces work together: - **The state class** holds the metadata for one slice: its name, its default value and the methods that change it. - **The action class** describes something that happened, such as a guest picking a room. It is a plain TypeScript class with a `static readonly type` string and, optionally, constructor fields for its payload. - **The action handler** is a method on the state class decorated with `@Action(...)`. NGXS calls it when a matching action is dispatched. There is no `switch` over action types and no separate reducer file; NGXS keeps a registry from each action's `type` string to the handlers that declared it. ## Declaring the state ```ts export interface RoomsStateModel { rooms: Room[]; selectedRoomId: string | null; } @State<RoomsStateModel>({ name: 'rooms', defaults: { rooms: [], selectedRoomId: null } }) @Injectable() export class RoomsState {} ``` The options on `@State` are: - **`name`** (required): the key under which this slice lives in the single global state object. It must be unique across the application and safe as an object property name, so no dashes or dots. A `StateToken` can replace the string to give the name a type. - **`defaults`**: the initial value of the slice. - **`children`**: the old way to nest sub-states. It is **deprecated** and should not appear in new code. The class also carries `@Injectable()`. NGXS creates state classes through Angular's dependency injection, so a state can inject an `HttpClient` or a domain service in its constructor or with `inject()`. In development NGXS warns when a state class is not decorated with `@Injectable()` right after `@State()`. ## Writing an action handler ```ts export class SelectRoom { static readonly type = '[Rooms Page] Select Room'; constructor(public roomId: string) {} } // inside RoomsState @Action(SelectRoom) selectRoom(ctx: StateContext<RoomsStateModel>, action: SelectRoom) { ctx.patchState({ selectedRoomId: action.roomId }); } ``` The first argument, `StateContext<T>`, is scoped to this state's slice only: | Member | What it does | |---|---| | `getState()` | Returns the freshest value of this slice each time it is called | | `setState(value)` | Replaces the whole slice; also accepts a state operator | | `patchState(partial)` | Shallow-merges the given top-level properties into the slice | | `dispatch(action)` | Dispatches further actions and returns an Observable | | `abortSignal` | An `AbortSignal` for cancelling async work (NGXS 21 and later) | The handler does **not** return the new state the way a reducer does. It writes through the context. A return value matters only for async work: returning an Observable or a Promise tells NGXS to wait for it before the action counts as complete. One action can be handled by **several states**. A `CheckOutGuest` action can free a room in `RoomsState` and close a booking in `ReservationsState`; each handler gets a context for its own slice. ## Registering and dispatching in a standalone app NGXS 22 follows Angular 22 and is built around standalone providers: 1. In `app.config.ts`, add `provideStore([RoomsState])` to the application providers. An optional second argument takes options such as `developmentMode`, followed by plugin features such as `withNgxsReduxDevtoolsPlugin()`. 2. For states that belong to a lazily loaded route, add `provideStates([ReservationsState])` to that route's `providers`. `provideStore` must still be present at the root. 3. In a component, create a dispatcher with `private selectRoom = dispatch(SelectRoom);` and call `this.selectRoom(id)`, or inject `Store` and call `store.dispatch(new SelectRoom(id))`. `NgxsModule.forRoot()` and `NgxsModule.forFeature()` are still exported, but both are marked **deprecated** in favour of `provideStore()` and `provideStates()`. ## Mistakes interviewers listen for - Writing NGXS actions with NgRx's `createAction` and `props`. NGXS actions are classes with a static `type`. - Returning a new state object from an `@Action` method and expecting NGXS to store it. - Assuming `getState()` returns the whole application state; it returns this slice only. - Believing state classes cannot use services. They are built by DI and can inject them. - Describing `NgxsModule.forRoot` as the current setup in an NGXS 22 codebase. - Reusing one `type` string for two action classes. NGXS matches handlers by the string, not by the class, so the two actions would trigger each other's handlers. By convention the string names its source in brackets, as in `[Rooms Page] Select Room`, which keeps it unique and readable in DevTools.

  • Can two different NGXS state classes handle the same action?
    Yes. Any number of states can declare `@Action(SameAction)`. NGXS runs every registered handler, each with a `StateContext` scoped to its own slice, and the action completes when all of them have finished. That is how one `CheckOutGuest` action can free a room in `RoomsState` and close the booking in `ReservationsState` without either state calling the other.
  • Why does the NGXS state name have to be unique and property-safe?
    NGXS keeps every slice in one global state object, keyed by the state's `name`. Two states with the same name would collide on that key. The docs also require names that are safe as object property names, with no dashes or dots, because dot notation is used to address nested slices, for example in the storage plugin's `keys` option.

saying these in an interview costs you the question

  • NGXS actions are created with createAction and props, the same as NgRx.
  • An @Action handler must return the new state object, like a reducer.
  • ctx.getState() returns the whole application state tree.
  • NGXS state classes cannot inject services because DI does not create them.
  • NgxsModule.forRoot is the current way to register states in NGXS 22.