Managing state in Angular applications ensures a smooth and consistent user experience. Angular has some built-in options for managing simple states, but as your app gets more complicated, these may not be enough. That’s where state management libraries like NgRx, Akita, and NGXS come in. Each one gives you a different way to easily handle state.
But with so many choices, how do you know which one suits you best? In this article, I'll tell you the pros and cons of each, using both general information and my own experience. By the end, you'll have a better idea 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 gives you a quick look at each state management approach, so you can compare options quickly before going into more detail.
>> Read more:
- React vs Angular: A Comprehensive Side-by-Side Comparison
- What is MEAN Stack? A Detail Guide for Beginners
- MERN Stack vs MEAN Stack: What is Difference?
Component Inputs and Outputs (Component States)
Angular provides @Input and @Output decorators as part of its core framework. It's very simple to manage state between parent and child components in this way. The parent sends data to the child using @Input, and the child can send events back to the parent using @Output. This approach 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 theremoveTask()
method is called, allowing the parent to know when to remove the task.
- Receives a
-
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 theremove
event emitted by the child.
- Manages the list of tasks (
ngModel
ngModel is a built-in Angular directive (part of the FormsModule) that makes two-way data binding in forms easier. It automatically syncs form inputs with the component's state. This means that 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 that need to handle data quickly.
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 theTodoService
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 theTodoListComponent
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 thelistTodos()
method from the service and assigning the result to the component’stodos
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