Introduction
As Angular applications grow in complexity, state management becomes critical to maintaining predictable, maintainable, and scalable code. Over the years, developers have relied on libraries like NGRX and NGXS to handle complex state interactions, but with the arrival of Angular Signals and the Signals Store, a native, lightweight, and reactive alternative has emerged.
In this article, we’ll compare NGRX, NGXS, and Signals Store — their architecture, advantages, and use cases — and implement a practical example showing how the same problem can be solved using all three approaches.
The Evolution of State Management in Angular
| Approach | Introduced | Core Concept | Best For |
|---|
| NGRX | Angular 4+ | Redux pattern (actions → reducers → state) | Large-scale enterprise apps |
| NGXS | Angular 5+ | Simplified state handling using decorators | Medium-sized apps needing less boilerplate |
| Signals Store | Angular 17+ | Reactive signals-based store | Modern, lightweight applications |
1. NGRX – The Redux Way
NGRX uses a unidirectional data flow with three main parts:
Actions: events that describe “what happened”.
Reducers: pure functions that determine “how state changes”.
Selectors: retrieve slices of state efficiently.
Example: Managing a Todo List
Install NGRX
ng add @ngrx/store
Define Actions
// todo.actions.tsimport { createAction, props } from '@ngrx/store';
export const addTodo = createAction('[Todo] Add', props<{ task: string }>());
export const toggleTodo = createAction('[Todo] Toggle', props<{ id: number }>());
Create Reducer
// todo.reducer.tsimport { createReducer, on } from '@ngrx/store';
import { addTodo, toggleTodo } from './todo.actions';
export interface TodoState {
todos: { id: number; task: string; completed: boolean }[];
}
const initialState: TodoState = { todos: [] };
export const todoReducer = createReducer(
initialState,
on(addTodo, (state, { task }) => ({
...state,
todos: [...state.todos, { id: Date.now(), task, completed: false }]
})),
on(toggleTodo, (state, { id }) => ({
...state,
todos: state.todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
)
}))
);
Use in Component
@Component({
selector: 'app-ngrx-todo',
template: `
<input [(ngModel)]="task" placeholder="New task" />
<button (click)="addTask()">Add</button>
<ul>
<li *ngFor="let t of todos$ | async" (click)="toggle(t.id)">
{{ t.task }} - {{ t.completed ? '✅' : '❌' }}
</li>
</ul>
`
})
export class NgrxTodoComponent {
task = '';
todos$ = this.store.select(state => state.todos.todos);
constructor(private store: Store<{ todos: TodoState }>) {}
addTask() {
this.store.dispatch(addTodo({ task: this.task }));
this.task = '';
}
toggle(id: number) {
this.store.dispatch(toggleTodo({ id }));
}
}
✅ Pros
Predictable and testable.
Great DevTools for debugging.
Suitable for complex apps.
❌ Cons
2. NGXS – Simplified State Management
NGXS builds on NGRX concepts but uses decorators to simplify actions and state definition.
Example: Todo Management
Install NGXS
npm install @ngxs/store
Create State
// todo.state.tsimport { State, Action, StateContext, Selector } from '@ngxs/store';
export class AddTodo {
static readonly type = '[Todo] Add';
constructor(public task: string) {}
}
export class ToggleTodo {
static readonly type = '[Todo] Toggle';
constructor(public id: number) {}
}
export interface Todo {
id: number;
task: string;
completed: boolean;
}
@State<Todo[]>({
name: 'todos',
defaults: []
})
export class TodoState {
@Selector()
static getTodos(state: Todo[]) {
return state;
}
@Action(AddTodo)
add({ getState, setState }: StateContext<Todo[]>, { task }: AddTodo) {
const newTodo: Todo = { id: Date.now(), task, completed: false };
setState([...getState(), newTodo]);
}
@Action(ToggleTodo)
toggle({ getState, setState }: StateContext<Todo[]>, { id }: ToggleTodo) {
setState(
getState().map(t => (t.id === id ? { ...t, completed: !t.completed } : t))
);
}
}
Use in Component
@Component({
selector: 'app-ngxs-todo',
template: `
<input [(ngModel)]="task" placeholder="New task" />
<button (click)="add()">Add</button>
<ul>
<li *ngFor="let t of todos$ | async" (click)="toggle(t.id)">
{{ t.task }} - {{ t.completed ? '✅' : '❌' }}
</li>
</ul>
`
})
export class NgxsTodoComponent {
task = '';
todos$ = this.store.select(TodoState.getTodos);
constructor(private store: Store) {}
add() {
this.store.dispatch(new AddTodo(this.task));
this.task = '';
}
toggle(id: number) {
this.store.dispatch(new ToggleTodo(id));
}
}
✅ Pros
❌ Cons
3. Signals Store – The Native Angular Way
Starting with Angular 17, developers can manage app state using Signals Store, built on top of Angular Signals. It’s designed to be lightweight, reactive, and framework-native.
Example: Todo Management Using Signals Store
Install Signals Store (Angular 17+)
npm install @ngrx/signals
Create Store
// todo.store.tsimport { signalStore, withState, withMethods } from '@ngrx/signals';
interface Todo {
id: number;
task: string;
completed: boolean;
}
interface TodoState {
todos: Todo[];
}
const initialState: TodoState = { todos: [] };
export const TodoStore = signalStore(
{ providedIn: 'root' },
withState(initialState),
withMethods(store => ({
add(task: string) {
const newTodo: Todo = { id: Date.now(), task, completed: false };
store.setState({
todos: [...store.state().todos, newTodo]
});
},
toggle(id: number) {
store.setState({
todos: store.state().todos.map(t =>
t.id === id ? { ...t, completed: !t.completed } : t
)
});
}
}))
);
Use in Component
@Component({
selector: 'app-signals-todo',
template: `
<input [(ngModel)]="task" placeholder="New task" />
<button (click)="add()">Add</button>
<ul>
<li *ngFor="let t of store.todos()"
(click)="toggle(t.id)">
{{ t.task }} - {{ t.completed ? '✅' : '❌' }}
</li>
</ul>
`
})
export class SignalsTodoComponent {
task = '';
constructor(public store: TodoStore) {}
add() {
this.store.add(this.task);
this.task = '';
}
toggle(id: number) {
this.store.toggle(id);
}
}
✅ Pros
No external dependencies.
Fully reactive with Signals.
Very little boilerplate.
Ideal for modern Angular apps.
❌ Cons
When to Use Which?
| Scenario | Recommended Approach |
|---|
| Large enterprise-scale app with complex workflows | NGRX |
| Medium-sized app needing simplicity | NGXS |
| Modern Angular app (v17+) with Signals | Signals Store |
| Rapid prototyping or minimal dependencies | Signals Store |
Conclusion
Each state management solution has its strengths:
NGRX offers scalability and robust tooling.
NGXS provides simplicity and readability.
Signals Store delivers modern reactivity with minimal overhead.
As Angular continues to evolve, Signals Store is quickly becoming the go-to approach for most new projects, offering a clean, intuitive way to handle state without external libraries.
If you’re starting a new Angular project today, Signals Store is worth your attention—it’s fast, declarative, and feels truly native to Angular’s future.