State Management Basics and Practices
State management involves managing an application’s global state—data shared across multiple components. As application complexity grows, state management becomes critical for:
- Centralized State Management: Shared state should be managed in a single location rather than scattered across components.
- Predictability: State changes should be predictable, with each action leading to deterministic state updates.
- Debuggability: State management should provide mechanisms to track state changes for easier debugging.
- Testability: State management should be designed to facilitate testing, ensuring state changes meet expectations.
Practice: Using Angular Services for State Management
The simplest form of state management in Angular is using services. Services act as a communication bridge between components, storing and distributing data.
Creating a State Service
Create a state service:
// state.service.ts
import { Injectable } from '@angular/core';
@Injectable({
providedIn: 'root'
})
export class StateService {
private _count = 0;
get count() {
return this._count;
}
increment() {
this._count++;
}
}Injecting the Service
Inject the service into components that need access to the state:
// app.component.ts
import { Component } from '@angular/core';
import { StateService } from './state.service';
@Component({
selector: 'app-root',
template: `
<button (click)="increment()">Increment</button>
<p>Count: {{ count }}</p>
`
})
export class AppComponent {
constructor(public stateService: StateService) {}
get count() {
return this.stateService.count;
}
increment() {
this.stateService.increment();
}
}Using Akita for State Management
Akita is a state management library designed for Angular, offering a simple and predictable way to manage application state. Akita’s philosophy is “store everything,” meaning not only business data but also network request states, error messages, and more can be stored in the Akita Store.
Step 1: Install Akita
Install Akita in your Angular project:
npm install @datorama/akitaOr with Yarn:
yarn add @datorama/akitaStep 2: Initialize Akita
Initialize Akita in your project to create the Akita Store and necessary services:
ng add @datorama/akitaThis automatically adds Akita dependencies and configures Akita in app.module.ts.
Step 3: Create State Model and Store
In Akita, the state model is defined within a Store class. Define the state model and create a Store class:
// src/app/models/user.model.ts
export interface User {
id: number;
name: string;
email: string;
}
// src/app/stores/user.store.ts
import { EntityStore, StoreConfig } from '@datorama/akita';
import { User } from './models/user.model';
export interface UsersState {
list: User[];
loading: boolean;
error: string;
}
@StoreConfig({ name: 'users' })
export class UsersStore extends EntityStore<UsersState> {
constructor() {
super({
list: [],
loading: false,
error: ''
});
}
}Step 4: Generate CRUD Methods
Akita provides a feature to generate CRUD methods, simplifying state update code:
ng g @datorama/akita:crud usersThis generates methods like addUser, updateUser, deleteUser, loadUsers, and clearUsers for the UsersStore.
Step 5: Configure Akita Store
Ensure the Akita Store is correctly configured in app.module.ts:
// src/app/app.module.ts
import { NgModule } from '@angular/core';
import { AkitaNgDevtools } from '@datorama/akita-ngdevtools';
import { environment } from '../environments/environment';
import { UsersStoreModule } from './stores/users.store.module';
@NgModule({
imports: [
UsersStoreModule,
AkitaNgDevtools.forRoot({ enabled: !environment.production })
]
})
export class AppModule { }Step 6: Use Akita Store
Inject the Akita Store in components and use its methods to update state:
// src/app/components/user-list/user-list.component.ts
import { Component, OnInit } from '@angular/core';
import { UsersStore } from '../stores/users.store';
import { Observable } from 'rxjs';
@Component({
selector: 'app-user-list',
template: `
<ul>
<li *ngFor="let user of users$ | async">
{{ user.name }}
</li>
</ul>
`
})
export class UserListComponent implements OnInit {
users$: Observable<User[]>;
constructor(private usersStore: UsersStore) {
this.users$ = this.usersStore.select(state => state.list);
}
ngOnInit() {
this.usersStore.loadUsers();
}
}Step 7: Use Akita Effects
Akita Effects handle side effects, such as network requests, by listening to Store state changes and triggering actions:
// src/app/effects/user.effect.ts
import { Injectable } from '@angular/core';
import { Effect, Actions } from '@datorama/akita-ng-effects';
import { UsersStore } from './stores/users.store';
import { UserService } from './services/user.service';
@Injectable()
export class UserEffects {
@Effect()
loadUsers$ = this.actions$.pipe(
ofType(UsersStore.LOAD),
mergeMap(() => this.userService.getUsers().pipe(
map(users => UsersStore.ADD({ users })),
catchError(error => of(UsersStore.ERROR({ error })))
))
);
constructor(
private actions$: Actions,
private userService: UserService,
private usersStore: UsersStore
) {}
}Step 8: Use Akita Query
Akita Query is used to read data from the Store, offering methods like selectAll, selectEntity, and selectTotal:
// src/app/services/user.service.ts
import { Injectable } from '@angular/core';
import { UsersQuery } from './stores/users.query';
@Injectable()
export class UserService {
constructor(private usersQuery: UsersQuery) {}
getUsers() {
return this.usersQuery.selectAll();
}
}Using NgRx for State Management
NgRx is one of the most popular state management libraries in the Angular community, based on the Redux architecture pattern. NgRx includes Actions, Reducers, Effects, and Selectors.
Defining Actions
Actions are objects that describe events in the application:
// actions.ts
export enum ActionTypes {
Increment = '[Counter] Increment',
Decrement = '[Counter] Decrement'
}
export class Increment {
readonly type = ActionTypes.Increment;
}
export class Decrement {
readonly type = ActionTypes.Decrement;
}
export type CounterActions = Increment | Decrement;Creating Reducers
Reducers are pure functions that take the current state and an action, returning a new state:
// reducers.ts
import { createReducer, on } from '@ngrx/store';
import { CounterActions, ActionTypes } from './actions';
export interface State {
count: number;
}
const initialState: State = {
count: 0
};
export const counterReducer = createReducer(
initialState,
on(Increment, (state) => ({ ...state, count: state.count + 1 })),
on(Decrement, (state) => ({ ...state, count: state.count - 1 }))
);Setting Up the Store
Configure the NgRx Store in the application module:
// app.module.ts
import { NgModule } from '@angular/core';
import { StoreModule } from '@ngrx/store';
import { counterReducer } from './reducers';
@NgModule({
imports: [
StoreModule.forRoot({ counter: counterReducer })
]
})
export class AppModule { }Using Effects
Effects listen to Actions and can perform asynchronous operations:
// effects.ts
import { Injectable } from '@angular/core';
import { Actions, createEffect, ofType } from '@ngrx/effects';
import { exhaustMap, map } from 'rxjs/operators';
import { CounterActions } from './actions';
@Injectable()
export class CounterEffects {
increment$ = createEffect(() =>
this.actions$.pipe(
ofType(CounterActions.Increment),
exhaustMap(() => this.someService.increment())
)
);
constructor(private actions$: Actions, private someService: SomeService) {}
}Using Selectors
Selectors are functions that retrieve specific state slices from the Store:
// selectors.ts
import { createSelector } from '@ngrx/store';
import { State } from './reducers';
export const selectCount = createSelector(
(state: any) => state.counter,
(counterState: State) => counterState.count
);Using the Store in Components
Use the Store and Selector in components:
// app.component.ts
import { Component } from '@angular/core';
import { Store } from '@ngrx/store';
import { selectCount } from './selectors';
@Component({
selector: 'app-root',
template: `
<button (click)="increment()">Increment</button>
<p>Count: {{ count$ | async }}</p>
`
})
export class AppComponent {
count$ = this.store.select(selectCount);
constructor(private store: Store) {}
increment() {
this.store.dispatch(new Increment());
}
}



