Angular State Management


What is state management in Angular?

State management in Angular refers to handling the application's data and state across different components and modules. Managing state is crucial in larger applications where components share and update the same data. State management ensures consistency, performance, and predictability in how data flows through the application.

State can be handled locally (inside components), shared between components using services, or globally managed using libraries like NgRx or Akita.


What are the common approaches to managing state in Angular?

There are three common approaches to managing state in Angular:

  • Local component state: Data is managed locally within each component using component properties and Angular's two-way binding.
  • Shared service state: Services are used to share data between components. Services can act as a singleton where state is stored, allowing components to access and modify the same data.
  • Global state management: Libraries like NgRx, Akita, and NgXs are used to manage global application state using a centralized store that follows reactive programming principles.

What is a service-based state management approach in Angular?

Service-based state management in Angular uses services to store and manage shared state across multiple components. The service acts as a singleton, and its state can be updated by any component that injects the service. Components subscribe to the service to get notified of state changes.

Example of service-based state management:

import { Injectable } from '@angular/core';
import { BehaviorSubject } from 'rxjs';

@Injectable({
  providedIn: 'root'
})
export class StateService {
  private counterSource = new BehaviorSubject(0);
  counter$ = this.counterSource.asObservable();

  increment() {
    const currentCount = this.counterSource.value;
    this.counterSource.next(currentCount + 1);
  }

  decrement() {
    const currentCount = this.counterSource.value;
    this.counterSource.next(currentCount - 1);
  }
}

In this example, the StateService manages a counter state using a BehaviorSubject that emits new values whenever the state is updated.


What is NgRx in Angular?

NgRx is a reactive state management library for Angular based on Redux principles. It provides a centralized store to manage the global state of an application. NgRx uses actions, reducers, and effects to handle state changes in a predictable manner. It is ideal for large-scale applications where managing complex state across multiple components is needed.


What are the key concepts of NgRx?

NgRx is built on the following key concepts:

  • Store: The single source of truth that holds the application's state.
  • Actions: Plain objects that describe changes to the state. They are dispatched to the store to trigger updates.
  • Reducers: Pure functions that take the current state and an action to return a new state.
  • Selectors: Functions used to select specific pieces of state from the store.
  • Effects: Handle side effects (like API calls) outside the store and dispatch new actions based on the result of those effects.

How do you set up NgRx in an Angular application?

To set up NgRx, follow these steps:

  1. Install NgRx packages: npm install @ngrx/store @ngrx/effects.
  2. Define actions to describe the changes that can occur in the state.
  3. Create reducers to handle state updates based on actions.
  4. Set up the store in the application module.
  5. Use effects to handle side effects such as asynchronous operations.

Example of setting up NgRx:

import { Action, createReducer, on } from '@ngrx/store';
import { createAction } from '@ngrx/store';

// Actions
export const increment = createAction('[Counter] Increment');
export const decrement = createAction('[Counter] Decrement');

// Initial state
export const initialState = 0;

// Reducer
const _counterReducer = createReducer(
  initialState,
  on(increment, state => state + 1),
  on(decrement, state => state - 1)
);

export function counterReducer(state: number | undefined, action: Action) {
  return _counterReducer(state, action);
}

In this example, NgRx is set up with actions (increment and decrement), an initial state, and a reducer function to manage state changes.


What are actions in NgRx?

Actions in NgRx are plain objects that describe changes that can occur in the state. They are dispatched to the store and handled by reducers. Actions typically have a type field and can carry additional data as a payload.

Example of an action:

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

export const addTodo = createAction(
  '[Todo] Add Todo',
  props<{ title: string }>()
);

In this example, the addTodo action describes an operation to add a new todo item, with the title passed as a payload.


What are reducers in NgRx?

Reducers in NgRx are pure functions that take the current state and an action as arguments and return a new state based on the action. Reducers are responsible for updating the store's state in response to actions.

Example of a reducer:

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

export const initialState = [];

const _todoReducer = createReducer(
  initialState,
  on(addTodo, (state, { title }) => [...state, { title }])
);

export function todoReducer(state: any, action: any) {
  return _todoReducer(state, action);
}

In this example, the todoReducer handles the addTodo action by adding a new todo item to the state.


What are selectors in NgRx?

Selectors in NgRx are functions that extract specific pieces of state from the store. They allow components to subscribe to only the parts of the state they are interested in, improving performance and maintainability.

Example of a selector:

import { createSelector, createFeatureSelector } from '@ngrx/store';

export const selectTodos = createFeatureSelector<any>('todos');

export const selectTodoList = createSelector(
  selectTodos,
  (state) => state.todoList
);

In this example, the selectTodoList selector retrieves the list of todos from the state.


What are effects in NgRx?

Effects in NgRx handle side effects such as asynchronous operations (e.g., API calls) outside the store. Effects listen for dispatched actions and trigger new actions based on the results of side effects.

Example of an effect:

import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { EMPTY } from 'rxjs';
import { map, mergeMap, catchError } from 'rxjs/operators';
import { TodoService } from './todo.service';
import { loadTodos, todosLoaded } from './todo.actions';

@Injectable()
export class TodoEffects {

  loadTodos$ = createEffect(() => this.actions$.pipe(
    ofType(loadTodos),
    mergeMap(() => this.todoService.getAll()
      .pipe(
        map(todos => todosLoaded({ todos })),
        catchError(() => EMPTY)
      ))
  ));

  constructor(
    private actions$: Actions,
    private todoService: TodoService
  ) {}
}

In this example, the TodoEffects class listens for the loadTodos action and calls the TodoService to fetch todos. The result is mapped to a todosLoaded action, which updates the store with the loaded todos.


What are some popular alternatives to NgRx for state management in Angular?

Some popular alternatives to NgRx for state management in Angular include:

  • Akita: A state management pattern based on observables that provides a flexible and scalable approach to state management with less boilerplate compared to NgRx.
  • NgXs: A state management library that simplifies state handling in Angular by reducing boilerplate and using decorators to define actions and state changes.
  • Component store: A state management solution from NgRx for managing local component state with fewer requirements compared to the full NgRx setup.

When should you use global state management in Angular?

You should use global state management in Angular when your application has complex state requirements, such as:

  • Managing shared data across multiple components and modules.
  • Handling asynchronous data flows (e.g., API requests).
  • Keeping application state synchronized across routes or sessions.
  • Reducing complexity and avoiding props drilling in large applications.

For smaller applications, simpler solutions like service-based state management may suffice.

Ads