Skip to content

NGXS Fundamentals

NGXS is a state management library for Angular that provides a more intuitive and straightforward approach compared to Redux-based solutions. It uses decorators and classes to manage application state, making it easier to learn and implement.

NGXS excels at simplifying state management with minimal boilerplate while providing powerful features for managing complex application state.


NGXS uses decorators and a class-based approach, making the code more readable and easier to understand.

Compared to NgRx, NGXS requires significantly less code to set up and maintain.

Built-in debugging tools help you inspect state changes and time-travel through actions.

Efficient change detection and minimal overhead for most applications.

Active community with good documentation and examples.


A state is a class decorated with @State that holds your application data.

import { State, Action, StateContext } from '@ngxs/store';
export interface CounterStateModel {
count: number;
}
@State<CounterStateModel>({
name: 'counter',
defaults: {
count: 0
}
})
export class CounterState {
}

Actions are classes that represent events or user interactions.

export class Increment {
static readonly type = '[Counter] Increment';
constructor(public payload: number) {}
}
export class Decrement {
static readonly type = '[Counter] Decrement';
}
export class Reset {
static readonly type = '[Counter] Reset';
}

Selectors retrieve specific pieces of state.

import { Selector } from '@ngxs/store';
@State<CounterStateModel>({
name: 'counter',
defaults: { count: 0 }
})
export class CounterState {
@Selector()
static getCount(state: CounterStateModel): number {
return state.count;
}
}

Mutate state in response to actions.

@State<CounterStateModel>({
name: 'counter',
defaults: { count: 0 }
})
export class CounterState {
@Action(Increment)
increment(ctx: StateContext<CounterStateModel>, action: Increment) {
const state = ctx.getState();
ctx.setState({
...state,
count: state.count + action.payload
});
}
@Action(Decrement)
decrement(ctx: StateContext<CounterStateModel>) {
const state = ctx.getState();
ctx.setState({
...state,
count: state.count - 1
});
}
@Action(Reset)
reset(ctx: StateContext<CounterStateModel>) {
ctx.setState({ count: 0 });
}
}

export interface Todo {
id: string;
title: string;
completed: boolean;
}
export interface TodoStateModel {
todos: Todo[];
filter: 'all' | 'completed' | 'pending';
}
export class AddTodo {
static readonly type = '[Todo] Add Todo';
constructor(public payload: { title: string }) {}
}
export class RemoveTodo {
static readonly type = '[Todo] Remove Todo';
constructor(public payload: string) {}
}
export class ToggleTodo {
static readonly type = '[Todo] Toggle Todo';
constructor(public payload: string) {}
}
export class SetFilter {
static readonly type = '[Todo] Set Filter';
constructor(public payload: 'all' | 'completed' | 'pending') {}
}
import { State, Action, StateContext, Selector } from '@ngxs/store';
@State<TodoStateModel>({
name: 'todos',
defaults: {
todos: [],
filter: 'all'
}
})
export class TodoState {
@Selector()
static getTodos(state: TodoStateModel): Todo[] {
return state.todos;
}
@Selector()
static getFilter(state: TodoStateModel): string {
return state.filter;
}
@Selector()
static getFilteredTodos(state: TodoStateModel): Todo[] {
switch (state.filter) {
case 'completed':
return state.todos.filter(t => t.completed);
case 'pending':
return state.todos.filter(t => !t.completed);
default:
return state.todos;
}
}
@Action(AddTodo)
addTodo(ctx: StateContext<TodoStateModel>, action: AddTodo) {
const state = ctx.getState();
const newTodo: Todo = {
id: Math.random().toString(36),
title: action.payload.title,
completed: false
};
ctx.setState({
...state,
todos: [...state.todos, newTodo]
});
}
@Action(RemoveTodo)
removeTodo(ctx: StateContext<TodoStateModel>, action: RemoveTodo) {
const state = ctx.getState();
ctx.setState({
...state,
todos: state.todos.filter(t => t.id !== action.payload)
});
}
@Action(ToggleTodo)
toggleTodo(ctx: StateContext<TodoStateModel>, action: ToggleTodo) {
const state = ctx.getState();
ctx.setState({
...state,
todos: state.todos.map(t =>
t.id === action.payload ? { ...t, completed: !t.completed } : t
)
});
}
@Action(SetFilter)
setFilter(ctx: StateContext<TodoStateModel>, action: SetFilter) {
const state = ctx.getState();
ctx.setState({
...state,
filter: action.payload
});
}
}
import { Component } from '@angular/core';
import { Store } from '@ngxs/store';
import { TodoState, AddTodo, RemoveTodo, ToggleTodo, SetFilter } from './store';
@Component({
selector: 'app-todo-list',
template: `
<div class="todos">
<input #newTodo placeholder="Add a todo...">
<button (click)="add(newTodo.value)">Add</button>
<div class="filters">
<button (click)="filter('all')">All</button>
<button (click)="filter('pending')">Pending</button>
<button (click)="filter('completed')">Completed</button>
</div>
@for (todo of todos$ | async; track todo.id) {
<div class="todo">
<input
type="checkbox"
[checked]="todo.completed"
(change)="toggle(todo.id)"
>
<span [class.completed]="todo.completed">{{ todo.title }}</span>
<button (click)="remove(todo.id)">Delete</button>
</div>
}
</div>
`
})
export class TodoListComponent {
todos$ = this.store.select(TodoState.getFilteredTodos);
constructor(private store: Store) {}
add(title: string): void {
this.store.dispatch(new AddTodo({ title }));
}
remove(id: string): void {
this.store.dispatch(new RemoveTodo(id));
}
toggle(id: string): void {
this.store.dispatch(new ToggleTodo(id));
}
filter(type: 'all' | 'completed' | 'pending'): void {
this.store.dispatch(new SetFilter(type));
}
}

Terminal window
npm install @ngxs/store
import { NgxsModule } from '@ngxs/store';
import { TodoState } from './store/todo.state';
@NgModule({
imports: [
NgxsModule.forRoot([TodoState])
]
})
export class AppModule {}
import { bootstrapApplication } from '@angular/platform-browser';
import { provideStore } from '@ngxs/store';
import { TodoState } from './store/todo.state';
import { AppComponent } from './app.component';
bootstrapApplication(AppComponent, {
providers: [
provideStore([TodoState])
]
});

Handle side effects like API calls.

import { Action } from '@ngxs/store';
@State<DataStateModel>({
name: 'data',
defaults: { items: [], loading: false }
})
export class DataState {
@Action(LoadItems)
loadItems(ctx: StateContext<DataStateModel>) {
ctx.patchState({ loading: true });
return this.api.getItems().pipe(
tap(items => {
ctx.patchState({ items, loading: false });
}),
catchError(error => {
ctx.patchState({ loading: false });
return throwError(() => error);
})
);
}
}

Extend NGXS functionality with plugins for logging, dev tools, etc.

import { NgxsModule } from '@ngxs/store';
import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin';
import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
@NgModule({
imports: [
NgxsModule.forRoot([TodoState]),
NgxsReduxDevtoolsPluginModule.forRoot(),
NgxsLoggerPluginModule.forRoot()
]
})
export class AppModule {}

Do keep state normalized

{
todos: {
byId: { '1': todo1, '2': todo2 },
allIds: ['1', '2']
}
}

Do use selectors

@Selector()
static getTodoById(id: string) {
return (state: TodoStateModel) => state.todos[id];
}

Do handle side effects in effects

@Action(FetchData)
fetchData(ctx: StateContext, action: FetchData) {
return this.api.getData().pipe(
tap(data => ctx.setState(data))
);
}

Don’t mutate state directly

state.count++;
ctx.setState({ count: state.count + 1 });

Don’t put logic in components

todos = this.store.select(state =>
state.todos.filter(t => t.completed)
);
@Selector()
static getCompletedTodos(state): Todo[] {
return state.todos.filter(t => t.completed);
}

FeatureNGXSNgRx
Setup TimeMinutesHours
Learning CurveEasySteep
BoilerplateMinimalSignificant
Type SafetyGoodExcellent
DevToolsBuilt-inRedux DevTools
EcosystemSmallLarge
PerformanceGoodExcellent

NGXS is an excellent choice for Angular applications that need state management without the complexity of Redux-based solutions. Its intuitive API and minimal boilerplate make it ideal for teams new to state management.

When to use NGXS:

  • ✅ Small to medium projects
  • ✅ Teams new to state management
  • ✅ Want quick setup
  • ✅ Prefer simplicity over complexity

Consider NgRx when:

  • ❌ Need maximum type safety
  • ❌ Large, complex applications
  • ❌ Enterprise requirements
  • ❌ Already familiar with Redux

This guide is based on and inspired by: