Top 7 Popular Approaches for State Management in Angular

Component Inputs and Outputs, ngModel, Services, RxJS, NgRx, Akita, and NGXS are 7 key approaches for state management in Angular, suited for projects of all sizes.

Top 7 Popular Approaches for State Management in Angular

Managing state in Angular applications is key for ensuring a smooth and consistent user experience. While Angular provides some built-in options for basic state management, these can quickly become insufficient as your app grows in complexity. That’s where state management libraries like NgRx, Akita, and NGXS come in, each offering unique ways to handle state efficiently.

But with so many choices, how do you know which one suits your project best? In this article, I'll guide you through the pros and cons of each, combining general insights with my own hands-on experience. By the end, you'll have a better understanding of which approach fits your project’s needs. Let’s dive in!

Approach

Use Case

Pros

Cons

Learning Curve

Scalability

Component Inputs & Outputs

(Built-in Tool)

Simple parent-child communication

Easy to implement

Limited to small apps

Low

Low

ngModel

(Built-in Tool)

Form data binding

Automatic sync for forms

Only for form handling

Low

Low

Services

(Built-in Tool)

Shared state across components

Centralized logic, reusable

Harder to manage with complex state

Moderate

Medium

RxJS

(External Library)

Reactive, real-time updates

Powerful for complex, real-time data

Requires understanding of reactive programming

High

High

NgRx

(External Library)

Large-scale apps

Strong debugging and state tracking

Verbose code; lots of boilerplate

High

Very High

Akita

(External Library)

Medium-sized apps

Less boilerplate; easier state querying

Smaller community, less third-party support

Moderate

Medium to High

NGXS

(External Library)

Medium apps, fast setup

Minimal code, Angular-native approach

Limited ecosystem compared to NgRx

Moderate

Medium

This table provides a concise overview of each state management approach, helping you quickly compare options before diving into detailed exploration.

>> Read more:

Component Inputs and Outputs (Component States)

Angular provides @Input and @Output decorators as part of its core framework. This is one of the easiest ways to manage state between parent and child components. You send data from the parent to the child using @Input, and the child can communicate back by emitting events with @Output. This approach is straightforward and works best in small apps where state management only needs to happen between closely related components.

Example:

// todo-item.component.ts as a child component
import { Component, Input, Output, EventEmitter } from '@angular/core';

@Component({
  selector: 'app-todo-item',
  template: `
    <span>{{ task }}</span>
    <button (click)="removeTask()">Remove</button>
  `,
})
export class TodoItemComponent {
  @Input() task!: string; // Receiving the task from the parent
  @Output() remove = new EventEmitter<void>(); // Event to emit when removing

  removeTask() {
    this.remove.emit(); // Emit the remove event to the parent
  }
}
// todo-list.component.ts as a parent component
import { Component } from '@angular/core';

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
})
export class TodoListComponent {
  todos: string[] = []; // Array to store tasks
  newTodo: string = '';

  // Add a new task to the list
  addTodo() {
    if (this.newTodo.trim()) {
      this.todos.push(this.newTodo);
      this.newTodo = '';
    }
  }

  // Remove a task from the list
  removeTodo(index: number) {
    this.todos.splice(index, 1);
  }
}
<!-- todo-list.component.html -->
<h2>To-Do List</h2>

<input type="text" [(ngModel)]="newTodo" placeholder="Enter a new task" />
<button (click)="addTodo()">Add Task</button>

<ul>
  <li *ngFor="let todo of todos; index as i">
    <!-- Pass the todo and index to child component -->
    <app-todo-item [task]="todo" (remove)="removeTodo(i)"></app-todo-item>
  </li>
</ul>

Code Breakdown:

  • TodoItemComponent:

    • Receives a task (string) from the parent via @Input().
    • Emits a remove event using @Output() when the removeTask() method is called, allowing the parent to know when to remove the task.
  • TodoListComponent:

    • Manages the list of tasks (todos) and provides functionality to add or remove tasks.
    • Passes each task to the child TodoItemComponent via @Input and handles the removal using the remove event emitted by the child.

This approach allows the parent component to pass data to the child and react to events emitted by the child, providing clear and maintainable state management between the components.

ngModel

ngModel is a built-in Angular directive (part of the FormsModule) that simplifies two-way data binding in forms. It automatically syncs form inputs with the component's state, so when users change a form value, it’s instantly updated in the component, and any changes in the component also reflect back in the form. This makes it a perfect tool for form-heavy applications where efficient input handling is key.

In one of my recent projects, I used ngModel to manage user input for a dynamic to-do list app. By leveraging ngModel, I was able to instantly capture user input and synchronize it with the component’s state, eliminating the need for manual event listeners or complex state management.

Code Example:

// todo-list.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
})
export class TodoListComponent {
  newTodo: string = '';
}

newTodo is a string property defined in the TodoListComponent. It’s initialized as an empty string and is used to store the input data that the user enters in the form.

<!-- todo-list.component.html -->
<h2>To-Do List</h2>
<input type="text" [(ngModel)]="newTodo" placeholder="Enter a new task" />

Here, the ngModel directive binds the input field to the newTodo property using two-way data binding. This means:

  • User Input → Component: When the user types something into the input field, the newTodo property is updated with the current value of the input.
  • Component → User Input: Similarly, if the newTodo property is updated programmatically within the component, the change will automatically reflect in the input field.

Services

Angular services are a built-in feature of the framework that make managing and sharing state across multiple components easy and scalable. With Angular's dependency injection, you can store data or logic in a service and then inject it into any component that needs access. This centralized approach makes it much easier to scale as your application grows.

From my experience, services are ideal for medium-sized apps where you need to share state across unrelated components. They offer a much better alternative than relying on inputs and outputs alone. However, as your app becomes larger and more complex, services can become limited, and that's when you might need to turn to more structured solutions like state management libraries.

Code Example:

// todo.service.ts
import { Injectable } from '@angular/core';
import { TODOS } from './mock-todos';

@Injectable({
  // declares that this service should be created
  // by the root application injector.
  providedIn: 'root',
})
export class TodoService {
  listTodos() { return TODOS; }
}
  • @Injectable({ providedIn: 'root' }): This decorator makes the TodoService available throughout the app without needing to declare it in any specific component or module. It tells Angular to inject this service globally.
  • listTodos(): A simple method to fetch a list of to-do tasks from a mock data file (mock-todos).
// todo-list.component.ts
import { Component } from '@angular/core';

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
})
export class TodoListComponent {
  todos: string[] = []; // Array to store tasks
  constructor(
    private todoService: TodoService
  ) { }
  
  ngOnInit() {
	  // fetch all todos when component initializing
    this.todos = this.todoService.listTodos();
  }
}
  • TodoService Injection: The service is injected into the TodoListComponent through the constructor (private todoService: TodoService). This allows the component to access the methods and data provided by the service.
  • ngOnInit(): When the component is initialized, the list of to-do tasks is fetched by calling the listTodos() method from the service and assigning the result to the component’s todos array.

RxJS (BehaviorSubject and ReplaySubject)

RxJS is an external library that's tightly integrated with Angular, offering a reactive approach to state management, particularly useful for real-time applications. Two key utilities, BehaviorSubject and ReplaySubject, provide advanced control:

  • BehaviorSubject stores the latest value and instantly shares it with new subscribers.
  • ReplaySubject replays a specified number of past values, maintaining a short history of state changes.

These tools offer more flexibility for managing complex state flows compared to basic methods, making them ideal for handling real-time data updates.

Code Example of BehaviorSubject:

import { BehaviorSubject } from 'rxjs';

// Create a BehaviorSubject with an initial value
const subject = new BehaviorSubject(0); // Initial value is 0

// Subscribe to the BehaviorSubject
subject.subscribe({
  next: (value) => console.log(`Observer 1: ${value}`)
});

// Emit new values
subject.next(1);
subject.next(2);

// Another observer subscribing later
subject.subscribe({
  next: (value) => console.log(`Observer 2: ${value}`)
});

// Emit a new value again
subject.next(3);

// Output:
// Observer 1: 0
// Observer 1: 1
// Observer 1: 2
// Observer 2: 2   <-- Observer 2 gets the latest emitted value
// Observer 1: 3
// Observer 2: 3
  • BehaviorSubject remembers the most recent value emitted and immediately sends that value to any new subscribers.
  • Observer 2 subscribes after the value 2 has been emitted, and it receives the latest value (2) upon subscribing.

Code Example of ReplaySubject:

import { ReplaySubject } from 'rxjs';

// Create a ReplaySubject with a specified number of past values
const subject = new ReplaySubject(3);

// Subscribe to the BehaviorSubject
subject.subscribe({
  next: (value) => console.log(`Observer 1: ${value}`)
});

// Emit new values
subject.next(1);
subject.next(2);

// Another observer subscribing later
subject.subscribe({
  next: (value) => console.log(`Observer 2: ${value}`)
});

// Emit a new value again
subject.next(3);

// Output:
// Observer 1: 1
// Observer 1: 2
// Observer 2: 1 <-- Observer 2 gets the latest 3 emitted values
// Observer 2: 2 <-- Observer 2 gets the latest 3 emitted values
// Observer 1: 3
// Observer 2: 3
  • ReplaySubject stores a defined number of previously emitted values. New subscribers will receive the last n emitted values (in this case, the last 3 values).
  • Observer 2 receives all the previous values emitted before it subscribed, replaying the recent state.

While RxJS is powerful on its own, developers typically combine it with Angular Services as a more practical and popular method for state management in Angular. In this combination, RxJS handles reactive streams for real-time data updates, while services manage centralized state storage and distribution, offering control without adding complexity.

In practice, this approach is preferred over using RxJS or services independently, as it efficiently balances flexibility and structure for managing state in Angular applications. Here's an example:

// todo.service.ts
// TodoService Using BehaviorSubject
import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root', // Makes the service available across the app
})
export class TodoService {
  // Initial state of the to-do list is an empty array
  private todosSubject = new BehaviorSubject<string[]>([]);
  
  // Expose the todos as an observable so components can subscribe to it
  todos$ = this.todosSubject.asObservable();

  // Get the current value of the todos
  getTodos() {
    return this.todosSubject.getValue();
  }

  // Add a new todo
  addTodo(newTodo: string) {
    const currentTodos = this.getTodos(); // Get the current list of todos
    this.todosSubject.next([...currentTodos, newTodo]); // Add the new todo
  }
}
  • BehaviorSubject is used in the service to manage the state of the to-do list. It allows the service to store and emit the current list of todos while providing an observable (todos$) that components can subscribe to for real-time updates.
// todo-list.component.ts
import { Component, OnInit } from '@angular/core';
import { TodoService } from './todo.service';

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
})
export class TodoListComponent implements OnInit {
  todos: string[] = []; // Local array to store the tasks
  newTodo: string = ''; // Input model for the new task

  constructor(private todoService: TodoService) {}

  ngOnInit() {
    // Subscribe to the BehaviorSubject to get the current state of todos
    this.todoService.todos$.subscribe((todos) => {
      this.todos = todos; // Update the local todos array whenever the state changes
    });
  }

  // Add a task by interacting with the service
  addTodo() {
    if (this.newTodo.trim()) {
      this.todoService.addTodo(this.newTodo); // Call the service to add the new todo
      this.newTodo = ''; // Clear the input field
    }
  }
}
  • In this component, the TodoService is injected and used to manage the state of the to-do list. By subscribing to the BehaviorSubject exposed by the service (todos$), the component can reactively update its local state whenever the service emits new values (i.e., when a new to-do is added).
<!-- todo-list.component.html -->
<h2>To-Do List with BehaviorSubject</h2>

<input type="text" [(ngModel)]="newTodo" placeholder="Enter a new task" />
<button (click)="addTodo()">Add Task</button>

<ul>
  <li *ngFor="let todo of todos; index as i">
    {{ todo }}
  </li>
</ul>

NgRx

NgRx is a popular state management library in Angular that follows the Redux pattern, which uses a unidirectional data flow and manages state globally globally in a central store. The architecture of NgRx revolves around 4 main components: Actions, Reducers, Selectors, and Effects.

How NgRx works?

  • Actions: Represent events that change the state. For example, adding or removing a to-do:
// todo.actions.ts
import { createAction, props } from '@ngrx/store';

// Define actions for adding and removing a to-do
export const addTodo = createAction(
  '[Todo] Add Todo',
  props<{ todo: string }>() // Payload with the new to-do text
);

export const removeTodo = createAction(
  '[Todo] Remove Todo',
  props<{ index: number }>() // Payload with the index of the to-do to be removed
);
  • Reducers: Pure functions that take the current state and an action, then return a new state.
// todo.reducer.ts
import { createReducer, on } from '@ngrx/store';
import { addTodo, removeTodo } from './todo.actions';

// Initial state of the to-do list
export const initialState: string[] = [];

// Reducer function to handle state changes
export const todoReducer = createReducer(
  initialState,
  on(addTodo, (state, { todo }) => [...state, todo]), // Add a new to-do
  on(removeTodo, (state, { index }) => state.filter((_, i) => i !== index)) // Remove to-do by index
);
  • Selectors: Functions that retrieve specific slices of the state. You can create basic or advanced selectors to retrieve data as needed.
// todo.selectors.ts
import { createFeatureSelector, createSelector } from '@ngrx/store';

// Select the entire to-do state
export const selectTodos = createFeatureSelector<string[]>('todos');

// Optionally, create more specific selectors as needed
export const selectTodoCount = createSelector(
  selectTodos,
  (todos) => todos.length
);
  • Effects (Optional): Handle side effects, such as making API calls or performing asynchronous operations. They do not directly update the state but rather interact with actions.
// todo.effects.ts (Optional for more complex cases)
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { of } from 'rxjs';
import { map, switchMap } from 'rxjs/operators';
import { addTodo } from './todo.actions';

@Injectable()
export class TodoEffects {
  constructor(private actions$: Actions) {}

  // Example effect for handling side-effects (e.g., API requests)
  loadTodos$ = createEffect(() =>
    this.actions$.pipe(
      ofType(addTodo),
      switchMap(() => {
        // Simulate an async operation (e.g., API call)
        return of({ type: '[Todo] Add Todo Success' });
      })
    )
  );
}

How to Use NgRx in Your Angular App?

  • App Module Configuration: To start using NgRx, you need to configure your AppModule by registering the store and effects.

// app.module.ts
// Configure the Store
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { StoreModule } from '@ngrx/store';
import { todoReducer } from './todo.reducer';
import { EffectsModule } from '@ngrx/effects';
import { TodoEffects } from './todo.effects';

import { AppComponent } from './app.component';
import { TodoListComponent } from './todo-list/todo-list.component';

@NgModule({
  declarations: [AppComponent, TodoListComponent],
  imports: [
    BrowserModule,
    StoreModule.forRoot({ todos: todoReducer }), // Register the reducer
    EffectsModule.forRoot([TodoEffects]), // Register the effects (optional)
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}
  • Component Interaction with NgRx Store: Your components interact with the store by dispatching actions to modify the state or by selecting parts of the state to display.
// todo-list.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs';
import { addTodo, removeTodo } from './todo.actions';
import { selectTodos } from './todo.selectors';

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
})
export class TodoListComponent {
  todos$: Observable<string[]>; // Observable to the state
  newTodo: string = '';

  constructor(private store: Store) {
    this.todos$ = this.store.select(selectTodos); // Select the todos from the store
  }

  // Dispatch the addTodo action
  addTodo() {
    if (this.newTodo.trim()) {
      this.store.dispatch(addTodo({ todo: this.newTodo }));
      this.newTodo = ''; // Clear the input
    }
  }
}
  • Template Example: The template binds the todos$ observable to display the list of to-dos. It uses async pipes to automatically handle subscription and unsubscription.
<!-- todo-list.component.html -->
<h2>To-Do List with NgRx</h2>

<input type="text" [(ngModel)]="newTodo" placeholder="Enter a new task" />
<button (click)="addTodo()">Add Task</button>

<ul>
  <li *ngFor="let todo of todos$ | async; index as i">
    {{ todo }}
    <button (click)="removeTodo(i)">Remove</button>
  </li>
</ul>

NgRx has been my go-to library when working on large-scale applications that require a strong structure and predictable state transitions. The way it handles actions and reducers creates a clear flow, making debugging much easier. However, NgRx comes with a steep learning curve, especially with the amount of boilerplate code you need to write. If your app is relatively small, NgRx might feel like overkill.

Akita

Akita is another state management library in Angular, but compared to NgRx, it is designed to reduce boilerplate and offer a more straightforward API. Akita is ideal for applications where you want to manage state without the complexity of Redux-like architecture.

How Akita works:

  • Stores: Where the application state is kept.
  • Entities: Specialized stores that handle a collection of items (like a list of products).
  • Queries: Used to retrieve data from stores.
  • Actions: Unlike NgRx, Akita doesn’t require you to define explicit actions. Instead, you modify the state directly using service methods.

Akita Code Examples:

  • Defining the Store
// todos.store.ts
import { Injectable } from '@angular/core';
import { EntityState, EntityStore, StoreConfig } from '@datorama/akita';

// Define the shape of the to-do entity
export interface Todo {
  id: number;
  title: string;
  completed: boolean;
}

// Define the state that will hold an array of todos
export interface TodosState extends EntityState<Todo> {}

// Create the store to manage todos
@Injectable({ providedIn: 'root' })
@StoreConfig({ name: 'todos' }) // Name of the store
export class TodosStore extends EntityStore<TodosState, Todo> {
  constructor() {
    super(); // Initialize the store with an empty state
  }
}

Store: The TodosStore is an entity store that holds the state of the application (an array of to-dos). Akita uses an EntityState to manage collections of data.

  • Fetching Data with Queries
// todos.query.ts
import { Injectable } from '@angular/core';
import { QueryEntity } from '@datorama/akita';
import { TodosStore, TodosState } from './todos.store';

@Injectable({ providedIn: 'root' })
export class TodosQuery extends QueryEntity<TodosState> {
  // Query the state of the store
  constructor(protected store: TodosStore) {
    super(store);
  }

  // Get the list of completed todos
  selectCompletedTodos() {
    return this.selectAll({ filterBy: entity => entity.completed });
  }
}

Query: The TodosQuery retrieves the to-do list from the store. You can use selectCompletedTodos() to filter and retrieve only the completed tasks.

  • Modifying the Store with Services
// todos.service.ts
import { Injectable } from '@angular/core';
import { TodosStore } from './todos.store';
import { Todo } from './todos.store';
import { ID } from '@datorama/akita';

@Injectable({ providedIn: 'root' })
export class TodosService {
  constructor(private todosStore: TodosStore) {}

  // Add a new todo
  addTodo(title: string) {
    const newTodo: Todo = {
      id: Date.now(), // Use a unique id
      title,
      completed: false
    };
    this.todosStore.add(newTodo); // Add to store
  }
}

Service: The TodosService manages state-changing operations. Here, it adds a new to-do to the store by calling the add method on the TodosStore.

  • Component Using Akita
// todo-list.component.ts
import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { TodosQuery } from './todos.query';
import { TodosService } from './todos.service';
import { Todo } from './todos.store';

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
})
export class TodoListComponent {
  todos$: Observable<Todo[]>; // Observable for the to-do list

  newTodo: string = ''; // Input field for adding a new task

  constructor(private todosQuery: TodosQuery, private todosService: TodosService) {
    // Select all todos from the store
    this.todos$ = this.todosQuery.selectAll();
  }

  // Add a new task
  addTodo() {
    if (this.newTodo.trim()) {
      this.todosService.addTodo(this.newTodo);
      this.newTodo = ''; // Clear the input field
    }
  }
}

Component: The TodoListComponent subscribes to the state (an observable from the query) and reacts to state changes. It also calls the service to modify the state when a new task is added.

  • Display
<!-- todo-list.component.html -->
<h2>To-Do List with Akita</h2>

<input type="text" [(ngModel)]="newTodo" placeholder="Enter a new task" />
<button (click)="addTodo()">Add Task</button>

<ul>
  <li *ngFor="let todo of todos$ | async">
    <span [ngClass]="{ completed: todo.completed }">{{ todo.title }}</span>
  </li>
</ul>

Template: Uses the todos$ observable (returned from TodosQuery) to display the list of tasks. The async pipe automatically handles the subscription and updates the view as new tasks are added.

Akita offers an excellent balance between flexibility and ease of use. I found it to be a more lightweight option compared to NgRx, especially for medium-sized applications. You can achieve similar results with less boilerplate code. However, the documentation and community support aren’t as large as NgRx, so troubleshooting might be harder for edge cases.

NGXS

NGXS is another approach of state management in Angular without NgRx, offering a Redux-like experience but with significantly less boilerplate compared to NgRx. NGXS emphasizes simplicity and provides decorators to manage state in a more Angular-native way, which can make the learning curve easier for Angular developers.

How NGXS works:

  • State: Each piece of state is represented by a class decorated with @State().
  • Actions: Similar to NgRx but handled in a more declarative way using decorators.
  • Selectors: Like in NgRx, used to access parts of the state.
  • Effects (called Actions): Logic that updates the state in response to actions.

How to integrate NGXS into an Angular application?

  • Define Actions

Actions represent events in the application that change the state. In NGXS, actions are simple classes with a type and an optional payload.

// todo.actions.ts
export class AddTodo {
  static readonly type = '[Todo] Add';
  constructor(public payload: string) {}
}

export class RemoveTodo {
  static readonly type = '[Todo] Remove';
  constructor(public payload: number) {} // The index of the to-do to remove
}

export class ToggleTodo {
  static readonly type = '[Todo] Toggle';
  constructor(public payload: number) {} // The index of the to-do to toggle
}

These actions are dispatched by components to trigger changes in the application state. For example, AddTodo takes a payload of the new to-do item, while RemoveTodo and ToggleTodo use the index of the to-do in the list.

  • Create State

The state class is where the application’s state is defined and updated in response to actions. You can also define selectors to query specific parts of the state.

// todo.state.ts
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { AddTodo, RemoveTodo, ToggleTodo } from './todo.actions';

// Define the shape of a Todo item
export interface Todo {
  title: string;
  completed: boolean;
}

// Define the shape of the state
export interface TodoStateModel {
  todos: Todo[];
}

// The initial state of the to-do list
@State<TodoStateModel>({
  name: 'todos',
  defaults: {
    todos: [],
  },
})
export class TodoState {
  // Define selectors to query specific parts of the state
  @Selector()
  static getTodos(state: TodoStateModel) {
    return state.todos;
  }

  @Selector()
  static getCompletedTodos(state: TodoStateModel) {
    return state.todos.filter(todo => todo.completed);
  }

  // Handle adding a new todo
  @Action(AddTodo)
  addTodo(
    { getState, patchState }: StateContext<TodoStateModel>,
    { payload }: AddTodo
  ) {
    const state = getState();
    patchState({
      todos: [...state.todos, { title: payload, completed: false }],
    });
  }

  // Handle removing a todo by index
  @Action(RemoveTodo)
  removeTodo(
    { getState, patchState }: StateContext<TodoStateModel>,
    { payload }: RemoveTodo
  ) {
    const state = getState();
    const todos = state.todos.filter((_, i) => i !== payload);
    patchState({ todos });
  }

  // Handle toggling the completion status of a todo
  @Action(ToggleTodo)
  toggleTodo(
    { getState, patchState }: StateContext<TodoStateModel>,
    { payload }: ToggleTodo
  ) {
    const state = getState();
    const todos = state.todos.map((todo, i) =>
      i === payload ? { ...todo, completed: !todo.completed } : todo
    );
    patchState({ todos });
  }
}

The TodoState class manages the state of the to-do list. It provides methods for adding, removing, and toggling the completion status of tasks. Selectors are used to query specific parts of the state, such as retrieving all todos or only completed ones.

  • Set Up the App Module

In the app.module.ts, you need to register the NGXS store and include your state. This step sets up NGXS as the state management library for the application.

// app.module.ts
// Module Setup
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { FormsModule } from '@angular/forms';
import { NgxsModule } from '@ngxs/store';
import { TodoState } from './todo.state';

import { AppComponent } from './app.component';
import { TodoListComponent } from './todo-list/todo-list.component';

@NgModule({
  declarations: [AppComponent, TodoListComponent],
  imports: [
    BrowserModule,
    FormsModule,
    NgxsModule.forRoot([TodoState]), // Register the NGXS store
  ],
  providers: [],
  bootstrap: [AppComponent],
})
export class AppModule {}

Here, the NGXS store is registered globally with the NgxsModule.forRoot([TodoState]) method. The TodoState class is now the global state container for the application.

  • Integrate the Store with a Component

The TodoListComponent will interact with the NGXS store to dispatch actions (like adding or removing a to-do) and select data from the store (like retrieving the list of todos).

// todo-list.component.ts
import { Component } from '@angular/core';
import { Store, Select } from '@ngxs/store';
import { Observable } from 'rxjs';
import { AddTodo, RemoveTodo, ToggleTodo } from './todo.actions';
import { TodoState } from './todo.state';
import { Todo } from './todo.state';

@Component({
  selector: 'app-todo-list',
  templateUrl: './todo-list.component.html',
})
export class TodoListComponent {
  @Select(TodoState.getTodos) todos$: Observable<Todo[]>; // Observable for the to-do list

  newTodo: string = ''; // Input model for adding a new to-do

  constructor(private store: Store) {}

  // Dispatch action to add a new to-do
  addTodo() {
    if (this.newTodo.trim()) {
      this.store.dispatch(new AddTodo(this.newTodo));
      this.newTodo = ''; // Clear the input field
    }
  }

  // Dispatch action to remove a to-do by index
  removeTodo(index: number) {
    this.store.dispatch(new RemoveTodo(index));
  }

  // Dispatch action to toggle the completion status of a to-do
  toggleComplete(index: number) {
    this.store.dispatch(new ToggleTodo(index));
  }
}

This component interacts with the store by dispatching actions like AddTodo or RemoveTodo. It also uses the @Select() decorator to retrieve data from the store reactively and display it in the UI.

  • Component Template

The component's template is responsible for displaying the to-do list and triggering actions like adding, removing, or toggling a task.

<!-- todo-list.component.html -->
<h2>To-Do List with NGXS</h2>

<input type="text" [(ngModel)]="newTodo" placeholder="Enter a new task" />
<button (click)="addTodo()">Add Task</button>

<ul>
  <li *ngFor="let todo of todos$ | async; index as i">
    <span [ngClass]="{ completed: todo.completed }">{{ todo.title }}</span>
    <button (click)="toggleComplete(i)">
      {{ todo.completed ? 'Mark Incomplete' : 'Mark Complete' }}
    </button>
    <button (click)="removeTodo(i)">Remove</button>
  </li>
</ul>

The template uses the async pipe to automatically subscribe to the todos$ observable and display the list of tasks. Each task has buttons to toggle its completion status and to remove it from the list.

I’ve used NGXS on projects where the goal was rapid development without the complexity of NgRx. The state management is more intuitive thanks to the use of decorators, and the reduced boilerplate makes it faster to set up. While it’s great for small to medium-sized projects, I noticed that for more complex apps, it could lack some of the robustness provided by NgRx.

Angular State Management Best Practices

Choose the Right Approach:

  • Match the tool to your project size: Use built-in methods (like services and RxJS) for small to medium apps. For larger apps, consider NgRx, Akita, or NGXS to handle complex states.
  • Avoid unnecessary complexity: Don’t over-engineer state management if simpler solutions will work.

Centralize State Use a Central Store:

  • For larger apps, centralizing state ensures predictable and organized management.
  • Follow unidirectional flow: Use libraries like NgRx to keep state flow predictable and easier to debug.

Always Update State Immutably: Return new state objects rather than modifying the existing ones. This reduces bugs and ensures cleaner state transitions.

Organize by Feature:

  • Break down state: Use modular stores for each feature, making your state easier to manage as your app grows.
  • Lazy load feature state: Load state only when the feature is active to improve performance.

Use Selectors for Access

  • Don’t access state directly: Always use selectors to fetch specific slices of the state, optimizing performance and clarity.
  • Memoize selectors: Cache selector results to avoid unnecessary recalculations.

Keep Side eEffects out of Reducers: Use Effects or services for async operations like API calls, keeping business logic separate from state changes.

Unit Test Reducers, Selectors, and Effects: Ensure your state transitions and side effects work as expected, and mock external dependencies when testing.

Separate UI and Business Logic State: Only keep global state for business logic, while using local state for UI-specific concerns like loading indicators.

Clean Up State: Ensure no stale state remains after a feature is no longer in use by resetting it or using OnDestroy in components.

Optimize for Performance:

  • Throttle state updates: If handling frequent updates, debounce or throttle changes to prevent performance issues.
  • Use OnPush change detection: Reduce unnecessary re-renders by optimizing Angular’s change detection strategy.

FAQs

What is state management in Angular?

State management in Angular refers to the practice of controlling and synchronizing dynamic data (state) across components, services, and views. It ensures that changes in one part of an app are reflected in other parts that depend on that data, promoting consistency and maintainability.

Are Redux and NgRx the same?

Redux and NgRx follow the same pattern (unidirectional data flow), but NgRx is specifically designed for Angular. It integrates with Angular's ecosystem and includes additional features like Effects to handle side effects such as API calls.

What is the difference between NgRx and RxJS?

NgRx and RxJS serve different purposes. RxJS is for managing data streams reactively, while NgRx builds on top of RxJS to provide a full state management solution. NgRx is better for complex apps needing structured state management, while RxJS is suitable for reactive programming on its own.

What can I use instead of NgRx?

Some methods for state management in Angular without NgRx are Akita, NGXS, or even using RxJS with Angular Services for simpler apps. These solutions offer varying degrees of complexity and flexibility depending on the application's needs.

What is the most popular method for state management in Angular?

NgRx is often considered as the most popular choice for state management in Angular, especially large-scale enterprise applications, due to its structured, Redux-based approach and strong support for handling side effects through Effects.

>> Read more: Mastering React Context API for Streamlined Data Sharing

Conclusion

Choosing the right approach to state management in Angular is crucial to building scalable and maintainable applications. Consider key factors like the size of your project, team's experience, and the level of control you need over your application’s data flow. For smaller projects, built-in Angular tools like Services and ngModel might be sufficient, while larger, more complex applications may benefit from advanced libraries like NgRx, Akita, or NGXS.

Be open to experimenting with different approaches and ready to adjust your strategy as your app evolves. By carefully evaluating your requirements and selecting the right state management solution, you'll ensure efficient state handling and deliver a seamless, high-quality user experience.

>>> Follow and Contact Relia Software for more information!

  • web development
  • coding
  • development