State Management, in layman's terms, refers to a way we can store data, modify it, and react to its changes and make it available to the UI components.
Recently, I got a chance to work on state management in my angular application. I used
NGXS library for the same.
Just a brief note on NGXS, it is a state management pattern + library for Angular.
It acts as a single source of truth for your application's state, providing simple rules for predictable state mutations. NGXS is modeled after the CQRS pattern popularly implemented in libraries like Redux and NgRx but reduces boilerplate by using modern TypeScript features such as classes and decorators.
There are 4 major concepts to NGXS,
- Store: Global state container, action dispatcher and selector
- Actions: Class describing the action to take and its associated metadata
- State: Class definition of the state
- Selects: State slice selectors
Now, let's quickly start with our real work
FYI: I am working with the following configuration,
- Angular CLI: 11.2.5
- Node: 12.18.3
- OS: win32 x64
Create new Angular Application
- ng new state-management-with-ngxs-app
Enforce stricter type checking -> No
Add angular routing -> Yes
Stylesheet format -> css
Change directory and open solution in VS Code (you can use any editor that you prefer).
- cd state-management-with-ngxs-app
- code .
Install NGXS store
Now, in cmd or terminal of vs code, use the following command to install NGXS Store
- npm install @ngxs/store --save
Add imports in app.module.ts of an angular application
Import NgxsModule from @ngxs/store in app.module.ts
- import { NgModule } from '@angular/core';
- import { BrowserModule } from '@angular/platform-browser';
-
- import { AppRoutingModule } from './app-routing.module';
- import { AppComponent } from './app.component';
-
- import {NgxsModule} from '@ngxs/store';
-
- @NgModule({
- declarations: [
- AppComponent
- ],
- imports: [
- BrowserModule,
- AppRoutingModule,
- NgxsModule.forRoot()
- ],
- providers: [],
- bootstrap: [AppComponent]
- })
- export class AppModule { }
Install and import Bootstrap
For the sake of making our components look good in minimum efforts, we will make use of bootstrap.
Install bootstrap.
- npm install bootstrap --save
Import bootstrap by putting the following line in
styles.css
- @import "~bootstrap/dist/css/bootstrap.min.css"
Also, to have a model-driven approach to handle form inputs, we will use Reactive Forms.
Import ReactiveFormsModule in app.module.ts file. So now, your app.module.ts will would look like:
- import { NgModule } from '@angular/core';
- import { BrowserModule } from '@angular/platform-browser';
-
- import { AppRoutingModule } from './app-routing.module';
- import { AppComponent } from './app.component';
-
- import {NgxsModule} from '@ngxs/store';
- import { ReactiveFormsModule } from '@angular/forms';
-
-
- @NgModule({
- declarations: [
- AppComponent
- ],
- imports: [
- BrowserModule,
- AppRoutingModule,
- NgxsModule.forRoot(),
- ReactiveFormsModule
- ],
- providers: [],
- bootstrap: [AppComponent]
- })
- export class AppModule { }
Create components
In src -> app, create a new folder called components.
In cmd / terminal, navigate to the path of this components folder, and generate 2 new components: user-form and user-list.
- ng g c user-form
- ng g c user-list
Open the app.component.html file and add following code
- <div class="container">
- <div class="row">
- <div class="col-6">
- <app-user-form></app-user-form>
- </div>
- <div class="col-6">
- <app-user-list></app-user-list>
- </div>
- </div>
- </div>
Put following code in user-form.component.html
- <div class="card mt-5">
- <div class="card-body">
- <h5 class="card-title">Add / Edit user</h5>
- <form [formGroup]="userForm" (ngSubmit)="onSubmit()">
- <div class="form-group">
- <label>Name</label>
- <input type="text" class="form-control" formControlName="name" #name/>
- </div>
- <div class="form-group">
- <label>City</label>
- <input type="text" class="form-control" formControlName="city" #city/>
- </div>
- <div class="form-group">
- <button type="submit" class="btn btn-success mr-2">
- Save
- </button>
- <button class="btn btn-primary" (click)="clearForm()">
- Clear
- </button>
- </div>
- </form>
- </div>
- </div>
Put following code in user-form.component.ts
- import { Component, OnInit } from '@angular/core';
- import { User } from 'src/app/models/user';
- import { Observable, Subscription } from 'rxjs';
- import { Select, Store } from '@ngxs/store';
- import { UserState } from 'src/app/states/user.state';
- import { FormGroup, FormBuilder } from '@angular/forms';
- import { UpdateUser, AddUser, SetSelectedUser } from 'src/app/actions/user.action';
-
- @Component({
- selector: 'app-user-form',
- templateUrl: './user-form.component.html',
- styleUrls: ['./user-form.component.css']
- })
- export class UserFormComponent implements OnInit {
-
- @Select(UserState.getSelectedUser) selectedUser: Observable<User>;
-
- userForm: FormGroup;
- editUser = false;
- private formSubscription: Subscription = new Subscription();
-
- constructor(private fb: FormBuilder, private store: Store) {
- this.createForm();
- }
-
- ngOnInit() {
- this.formSubscription.add(
- this.selectedUser.subscribe(user => {
- if (user) {
- this.userForm.patchValue({
- id: user.id,
- name: user.name,
- city: user.city
- });
- this.editUser = true;
- } else {
- this.editUser = false;
- }
- })
- );
- }
-
- createForm() {
- this.userForm = this.fb.group({
- id: [''],
- name: [''],
- city: ['']
- });
- }
-
- onSubmit() {
- if (this.editUser) {
- this.store.dispatch(new UpdateUser(this.userForm.value, this.userForm.value.id));
- this.clearForm();
- } else {
- this.store.dispatch(new AddUser(this.userForm.value));
- this.clearForm();
- }
- }
-
- clearForm() {
- this.userForm.reset();
- this.store.dispatch(new SetSelectedUser(null));
- }
-
- }
Add following code in user-list.component.html
- <div class="col">
- <div *ngIf="(users| async)?.length > 0; else noRecords">
- <table class="table table-striped">
- <thead>
- <tr>
- <th>Name</th>
- <th>City</th>
- <th></th>
- <th></th>
- </tr>
- </thead>
- <tbody>
- <tr *ngFor="let user of users | async">
- <td>{{ user.name }}</td>
- <td>{{ user.city }}</td>
- <td>
- <button class="btn btn-primary" (click)="editUser(user)">Edit</button>
- </td>
- <td>
- <button class="btn btn-danger" (click)="deleteUser(user.id)">Delete</button>
- </td>
- </tr>
- </tbody>
- </table>
- </div>
- <ng-template #noRecords>
- <table class="table table-striped mt-5">
- <tbody>
- <tr>
- <td><p>No users to show!</p></td>
- </tr>
- </tbody>
- </table>
- </ng-template>
- </div>
Put following code in user-list.component.ts
- import { Component, OnInit } from '@angular/core';
- import { User } from 'src/app/models/user';
- import { Observable } from 'rxjs';
- import { Select, Store } from '@ngxs/store';
- import { UserState } from 'src/app/states/user.state';
- import { DeleteUser, SetSelectedUser } from 'src/app/actions/user.action';
-
- @Component({
- selector: 'app-user-list',
- templateUrl: './user-list.component.html',
- styleUrls: ['./user-list.component.css']
- })
- export class UserListComponent implements OnInit {
-
- @Select(UserState.getUserList) users: Observable<User[]>;
-
- constructor(private store: Store) {
- }
-
- ngOnInit() { }
-
- deleteUser(id: number) {
- this.store.dispatch(new DeleteUser(id));
- }
-
- editUser(payload: User) {
- this.store.dispatch(new SetSelectedUser(payload));
- }
-
-
- }
Create model
In src -> app, create new folder called models.
Inside that folder create a new file called user.ts
Put following code in user.ts file
- export interface User {
- id: number;
- name: string;
- city: string;
- }
Create Actions
Inside src > app, create a new folder action. Inside this folder, create a new file user.action.ts.
Add following code to user.action.ts
- import { User } from '../models/user';
-
- export class AddUser {
- static readonly type = '[User] Add';
-
- constructor(public payload: User) {
- }
- }
-
- export class UpdateUser {
- static readonly type = '[User] Update';
-
- constructor(public payload: User, public id: number) {
- }
- }
-
- export class DeleteUser {
- static readonly type = '[User] Delete';
-
- constructor(public id: number) {
- }
- }
-
- export class SetSelectedUser {
- static readonly type = '[User] Set';
-
- constructor(public payload: User) {
- }
- }
Create States
Inside src > app, create a new folder states. Inside this folder, create a new file user.state.ts.
Add following code to user.state.ts
- import { User } from '../models/user';
- import { State, Selector, Action, StateContext } from '@ngxs/store';
- import { AddUser, UpdateUser, DeleteUser, SetSelectedUser } from '../actions/user.action';
- import { Injectable } from '@angular/core';
-
- export class UserStateModel {
- Users: User[];
- selectedUser: User;
- }
-
- @State<UserStateModel>({
- name: 'Users',
- defaults: {
- Users: [],
- selectedUser: null
- }
- })
-
- @Injectable()
- export class UserState {
-
- constructor() {
- }
-
- @Selector()
- static getUserList(state: UserStateModel) {
- return state.Users;
- }
-
- @Selector()
- static getSelectedUser(state: UserStateModel) {
- return state.selectedUser;
- }
-
- @Action(AddUser)
- addUser({getState, patchState}: StateContext<UserStateModel>, {payload}: AddUser) {
- const state = getState();
- const UserList = [...state.Users];
- payload.id = UserList.length + 1;
- patchState({
- Users: [...state.Users, payload]
- });
- return;
- }
-
- @Action(UpdateUser)
- updateUser({getState, setState}: StateContext<UserStateModel>, {payload, id}: UpdateUser) {
- const state = getState();
- const UserList = [...state.Users];
- const UserIndex = UserList.findIndex(item => item.id === id);
- UserList[UserIndex] = payload;
- setState({
- ...state,
- Users: UserList,
- });
- return;
- }
-
-
- @Action(DeleteUser)
- deleteUser({getState, setState}: StateContext<UserStateModel>, {id}: DeleteUser) {
- const state = getState();
- const filteredArray = state.Users.filter(item => item.id !== id);
- setState({
- ...state,
- Users: filteredArray,
- });
- return;
- }
-
- @Action(SetSelectedUser)
- setSelectedUserId({getState, setState}: StateContext<UserStateModel>, {payload}: SetSelectedUser) {
- const state = getState();
- setState({
- ...state,
- selectedUser: payload
- });
- return;
- }
- }
Now, save everything and start the application with npm start / ng serve.
Open your browser and go to http://localhost:4200
You should be able to see the application as follows:
Fill in the name and city and click save.
The new record would be seen in the right-hand side table.
Similarly, you can add multiple users.
Also, users can be edited / deleted with the help of the buttons provided in the users list table.
Working
The
Store is a global state manager that dispatches actions your state containers listen to and provides a way to select data slices out from the global state.
We injected the store into our components, and used it for dispatching required actions.
Actions can either be thought of as a command which should trigger something to happen, or as the resulting event of something that has already happened.
An action may or may not have metadata (payload).
In our application, we have used various actions for adding users, updating and deleting them, etc.
States are classes that define a state container. Our states listen to actions via an @Action decorator.
Selects are functions that slice a specific portion of state from the global state container.
Hope this is helpful. If I have missed anything, let me know in the comments and I'll add it in!
https://github.com/saketadhav/angular11-state-management-with-ngxs-app
Happy Coding!