Skip to main content

NgRx Basics

What is NgRx?

NgRx is a state management library for Angular based on Redux principles. It provides a predictable state container using RxJS.

Core Concepts

Store

Central state container - single source of truth for your app state.

Actions

Events that describe what happened in the app.

import { createAction, props } from '@ngrx/store';

export const loadTodos = createAction('[Todo] Load Todos');
export const addTodo = createAction(
'[Todo] Add Todo',
props<{ text: string }>()
);
export const toggleTodo = createAction(
'[Todo] Toggle Todo',
props<{ id: string }>()
);

Reducers

Pure functions that take the current state and an action, return new state.

import { createReducer, on } from '@ngrx/store';
import * as TodoActions from './todo.actions';

export interface Todo {
id: string;
text: string;
completed: boolean;
}

export interface TodoState {
todos: Todo[];
}

const initialState: TodoState = {
todos: [],
};

export const todoReducer = createReducer(
initialState,
on(TodoActions.addTodo, (state, { text }) => ({
...state,
todos: [
...state.todos,
{ id: Date.now().toString(), text, completed: false },
],
})),
on(TodoActions.toggleTodo, (state, { id }) => ({
...state,
todos: state.todos.map(todo =>
todo.id === id ? { ...todo, completed: !todo.completed } : todo
),
}))
);

Selectors

Functions to select slices of state.

import { createSelector, createFeatureSelector } from '@ngrx/store';
import { TodoState } from './todo.reducer';

export const selectTodoState = createFeatureSelector<TodoState>('todos');

export const selectAllTodos = createSelector(
selectTodoState,
state => state.todos
);

export const selectCompletedTodos = createSelector(selectAllTodos, todos =>
todos.filter(todo => todo.completed)
);

Setup

1. Install NgRx

npm install @ngrx/store @ngrx/store-devtools

2. Configure Store

// app.config.ts
import { ApplicationConfig } from '@angular/core';
import { provideStore } from '@ngrx/store';
import { provideStoreDevtools } from '@ngrx/store-devtools';
import { todoReducer } from './state/todo.reducer';

export const appConfig: ApplicationConfig = {
providers: [
provideStore({ todos: todoReducer }),
provideStoreDevtools({ maxAge: 25 }),
],
};

3. Use in Components

import { Component, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import * as TodoActions from './state/todo.actions';
import * as TodoSelectors from './state/todo.selectors';
import { Todo } from './state/todo.reducer';

@Component({
selector: 'app-todos',
standalone: true,
template: `
<input #input (keyup.enter)="addTodo(input.value); input.value = ''" />
<ul>
@for (todo of todos$ | async; track todo.id) {
<li (click)="toggleTodo(todo.id)" [class.completed]="todo.completed">
{{ todo.text }}
</li>
}
</ul>
`,
})
export class TodosComponent {
private store = inject(Store);
todos$: Observable<Todo[]> = this.store.select(TodoSelectors.selectAllTodos);

addTodo(text: string) {
if (text.trim()) {
this.store.dispatch(TodoActions.addTodo({ text }));
}
}

toggleTodo(id: string) {
this.store.dispatch(TodoActions.toggleTodo({ id }));
}
}

When to Use NgRx

Use NgRx when:

  • State is shared across many components
  • You need time-travel debugging
  • Complex state updates require predictability
  • Team prefers explicit state management

Don't use NgRx when:

  • Simple local component state
  • Small applications (use Signals instead)
  • Only 1-2 components share state

NgRx with Signals (Modern Approach)

import { Component, inject } from '@angular/core';
import { Store } from '@ngrx/store';
import { toSignal } from '@angular/core/rxjs-interop';

@Component({
selector: 'app-todos',
standalone: true,
template: `
<ul>
@for (todo of todos(); track todo.id) {
<li>{{ todo.text }}</li>
}
</ul>
`,
})
export class TodosComponent {
private store = inject(Store);
todos = toSignal(this.store.select(TodoSelectors.selectAllTodos), {
initialValue: [],
});
}

Key Principles

  1. Single source of truth - State lives in one store
  2. State is read-only - Only actions can change state
  3. Pure functions - Reducers must be pure
  4. Immutability - Always return new state objects

Quick Reference

// Dispatch action
this.store.dispatch(myAction({ data }));

// Select state
this.data$ = this.store.select(mySelector);

// Select as signal
this.data = toSignal(this.store.select(mySelector));

Resources