Cómo implementar NGXS en tu proyecto Angular

Hola a todos, hoy os voy a explicar como funciona NGXS en angular.

NGXS es una state management para aplicaciones Angular. Nos permite gestionar el estado de una aplicación, centralizando el estado pudiendo realizar cambios de manera controlada.

Esta basado en el patrón Flux que se utiliza para gestionar el estado de una aplicación en un flujo unidireccional.

Pero en NGXS se simplifica, sin necesidad de pasar por la vista.

Partes de NGXS Store

  • Estado: es una representación inmutable de toda la información relevante de la aplicación en un momento dado.
  • Store: contiene el estado de la aplicación y nos permite actualizar y recuperar esos estados.
  • Actions: se usan para actualizar el estado y enviarlo a la store usando dispatch
  • Clases State: clases que representan el estado de la aplicación donde actúa el store.
  • Selectors: métodos para obtener una parte especifica del estado de la aplicación, permitiendo estar al tanto de las actualizaciones de ese estado.

Beneficios

  1. Centralización del estado: centraliza el estado de la aplicación en un solo lugar, facilitando la comprensión y el mantenimiento de la aplicación.
  2. Flujo unidireccional: utiliza el patrón de diseño Flux para gestionar el estado de la aplicación en un flujo unidireccional, haciendo que sea más predecible y fácil de depurar.
  3. Inmutabilidad del estado: se utilizan técnicas de inmutabilidad del estado para garantizar que no se modifique directamente, lo que previene errores y hace que la aplicación sea más robusta.
  4. Reducción de la complejidad: reduce la complejidad de la lógica de la aplicación y facilitando su mantenimiento.

Implementación en Angular

Para implementarlo en angular, seguiremos los siguientes pasos:

Lo primero crear nuestro proyecto, te dejo un tutorial donde lo explicamos.

Crear y ejecutar un proyecto angular

Creamos una componente de prueba:

$ ng g c people --skip-tests=true

Recuerda de añadir el componente en app.module:

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { PeopleComponent } from './people/people.component';

@NgModule({
  declarations: [
    AppComponent,
    PeopleComponent
  ],
  imports: [
    BrowserModule,
    AppRoutingModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Instalamos el cliente de ngxs con el siguiente comando:

$ npm i -g @ngxs/cli

Esto nos permitirá generar la base de las acciones y el estado. Lo hacemos global con -g para usarlo en cualquier proyecto.

Ahora, crearemos los ficheros de acciones y state para nuestro componente:

$ ngxs --name people --directory src/app/people --spec false

Te creará una carpeta llamada state con el siguiente contenido:

Creamos una clase Person dentro del componente:

export class Person {
    name: string;
    surname: string;
    age: number;
}

Instalamos @ngxs/store con el siguiente comando:

$ npm i  @ngxs/store

Vamos a app.module e importamos el modulo NgxsModule. Usamos el método forRoot y le pasamos un array con las clases de estado que queramos importar, en nuestro caso PeopleState.

import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { NgxsModule } from '@ngxs/store';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { PeopleComponent } from './people/people.component';
import { PeopleState } from './people/state/people.state';

@NgModule({
  declarations: [
    AppComponent,
    PeopleComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    NgxsModule.forRoot(
      [
        PeopleState
      ]
    )
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }

Vamos al fichero people.actions y adaptamos la acción por defecto por la siguiente:

export class GetPeople {
  static readonly type = '[People] Get people';
  constructor(public payload: { name: string }) { }
}

Es importante saber que la propiedad type de cada acción debe ser diferentes entre sí, ya que si puede lanzar más de una acción a la vez, normalmente se le suele poner como un prefijo ([People]).

En el constructor, añadimos un payload con un objeto donde le insertaremos los parámetros que queramos pasar, sino necesitas parámetros, no es necesario ponerlo.

Ahora, vamos al fichero people.state vamos a adaptarlo a la acción que queremos y la clase Person.


import { Injectable } from '@angular/core';
import { State, Action, StateContext } from '@ngxs/store';
import { Person } from '../model/person';
import { GetPeople } from './people.actions';

export class PeopleStateModel {
  public people: Person[];
}

const defaults = {
  people: []
};

@State({
  name: 'people',
  defaults
})
@Injectable()
export class PeopleState {
  @Action(GetPeople)
  getPeople({ setState }: StateContext, { payload }: GetPeople) {

  }
}

Explicamos algunos detalles:

  • PeopleStateModel: Propiedades del estado de People, añadiremos todo lo que necesitemos. En nuestro caso, las personas que vamos a almacenar.
  • defaults: Valores por defecto de las propiedades del estado.
  • @Action(<accion>): indica la acción que se ejecutará pasándole el objeto StateContext con los métodos a utilizar y el payload si lo tiene.

Todo State tiene que tener un nombre asociado, en nuestro caso people.

Ahora vamos a crear un servicio donde vamos a almacenar los datos que queremos recoger o filtrar. También puede ser útil para una futura integración con un backend.

$ ng g s people --skip-tests=true --path=src/app/services

Copia el siguiente código y pegalo en el fichero que se ha creado.


import { Injectable } from '@angular/core';
import { of } from 'rxjs';
import { Person } from '../people/model/person';

@Injectable({
  providedIn: 'root'
})
export class PeopleService {

  // Datos
  public people: Person[] = [
    {
      name: "Fernando",
      surname: "Ureña",
      age: 33
    },
    {
      name: "Hernando",
      surname: "Caballero",
      age: 20
    },
    {
      name: "Bernando",
      surname: "Torres",
      age: 45
    },
    {
      name: "Orlando",
      surname: "Reyes",
      age: 51
    }
  ];

  fetchPeople(name: string) {
    // Filtramos los elementos
    // Con of, creamos un observable a partir del array
    return of<Person[]>(this.people.filter(p => p.name.toLocaleLowerCase().includes(name.toLocaleLowerCase())));
  }
}

Volvemos al fichero people.state, inyectamos el nuevo servicio y llamamos a la función fetchPeople.

import { Injectable } from '@angular/core';
import { State, Action, StateContext } from '@ngxs/store';
import { tap } from 'rxjs';
import { PeopleService } from 'src/app/services/people.service';
import { Person } from '../model/person';
import { GetPeople } from './people.actions';

export class PeopleStateModel {
  public people: Person[];
}

const defaults = {
  people: []
};

@State({
  name: 'people',
  defaults
})
@Injectable()
export class PeopleState {

  constructor(private peopleService: PeopleService){ }

  @Action(GetPeople)
  getPeople({ setState }: StateContext, { payload }: GetPeople) {
    return this.peopleService.fetchPeople(payload.name).pipe(
      tap( (people: Person[]) => {
        setState({
          people
        })
      })
    )
  }
}

Con setState actualizamos las propiedades del estado.

Con esto tenemos lo básico para recoger las acciones y ejecutarlas pero aun no tenemos la conexión con el componente. Para ello, en el fichero people.state, añadiremos un @Selector.

import { Injectable } from '@angular/core';
import { State, Action, StateContext, Selector } from '@ngxs/store';
import { tap } from 'rxjs';
import { PeopleService } from 'src/app/services/people.service';
import { Person } from '../model/person';
import { GetPeople } from './people.actions';

export class PeopleStateModel {
  public people: Person[];
}

const defaults = {
  people: []
};

@State({
  name: 'people',
  defaults
})
@Injectable()
export class PeopleState {

  @Selector()
  static people(state: PeopleStateModel) {
    return state.people;
  }

  constructor(private peopleService: PeopleService) { }

  @Action(GetPeople)
  getPeople({ setState }: StateContext, { payload }: GetPeople) {
    return this.peopleService.fetchPeople(payload.name).pipe(
      tap((people: Person[]) => {
        setState({
          people
        })
      })
    )
  }
}

Este @Selector nos permite obtener una de las propiedades del estado.

Volvemos al componente, a su ts.

Necesitaremos los siguiente:

  • Un array donde almacenar las personas que obtengamos.
  • Asociar la propiedad people con un @Select mediante un observable.
  • Desencadenar la acción

import { Component, OnInit } from '@angular/core';
import { Select, Store } from '@ngxs/store';
import { Observable } from 'rxjs';
import { Person } from './model/person';
import { GetPeople } from './state/people.actions';
import { PeopleState } from './state/people.state';

@Component({
  selector: 'app-people',
  templateUrl: './people.component.html',
  styleUrls: ['./people.component.scss']
})
export class PeopleComponent implements OnInit {

  // Selector asociado a la propiedad people del estado
  @Select(PeopleState.people)
  people$: Observable<Person[]>;

  // Array donde almacenaremos las personas
  public peopleFiltered: Person[] = [];

  // Atributo para el filtro del nombre
  public filterName: string;
 
  constructor(private store: Store) { 
    this.filterName = '';
  }

  ngOnInit() {
    // Filtro al inicio
    this.filter();
    this.fetchPeople();
  }

  fetchPeople(){
    // Nos subscribimos para estar pendiente de los cambios de la propiedad
    this.people$.subscribe({
      next: () => {
        // Obtenemos el array de personas de la store
        this.peopleFiltered = this.store.selectSnapshot(PeopleState.people);
        console.log('People ha cambiado');
      }
    })
  }

  filter() {
    // Activo la acción, dandole el nombre a filtrar
    this.store.dispatch(new GetPeople({ name: this.filterName }));
  }
}

En la parte HTML, mostraremos el filtro y una tabla con los datos de la persona.

</pre>
<form><label for="name">Nombre</label> <input id="name" class="form-control" name="name" type="text" placeholder="Escribe un nombre..." /> <button class="btn btn-primary" type="submit">Filtrar</button></form>
<table>
<tbody>
<tr>
<th>Nombre</th>
<th>Apellidos</th>
<th>Edad</th>
</tr>
<tr>
<td>{{person.name}}</td>
<td>{{person.surname}}</td>
<td>{{person.age}}</td>
</tr>
</tbody>
</table>
<pre>

Puedes mejorarlo con bootstrap: https://www.discoduroderoer.es/instalar-bootstrap-4-en-nuestra-aplicacion-angular/ Como estamos usando ngModel, debemos importar FormsModule en nuestro app.module.


import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { NgxsModule } from '@ngxs/store';

import { AppRoutingModule } from './app-routing.module';
import { AppComponent } from './app.component';
import { PeopleComponent } from './people/people.component';
import { PeopleState } from './people/state/people.state';
import { FormsModule } from '@angular/forms';

@NgModule({
  declarations: [
    AppComponent,
    PeopleComponent,
  ],
  imports: [
    BrowserModule,
    AppRoutingModule,
    FormsModule,
    NgxsModule.forRoot(
      [
        PeopleState
      ]
    )
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }


No olvidemos las rutas (app.routing.ts):

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { PeopleComponent } from './people/people.component';

const routes: Routes = [
  { path: 'people-list', component: PeopleComponent },
  { path: '**', pathMatch: 'full', redirectTo: 'people-list'}
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})
export class AppRoutingModule { }

Ni del fichero app.component.html:


Ejecutamos nuestro proyecto:

$ npm start

Este es el resultado:

Fíjate que cada vez que filtramos, se ejecutan los siguientes pasos empezando desde el componente:

  • Nos subscribimos a la propiedad del estado que queremos mostrar.
  • Lanzamos la acción.
  • La store llama a la acción correspondiente usando el servicio que hemos creado.
  • El servicio nos filtra las personas.
  • La store modifica el estado.
  • La subscripción del primer paso, salta y actualizamos los datos.

Aquí te dejo un video donde explico un ejemplo de NGXS.

https://youtu.be/OQLjEu8X0X0

También el repositorio con el ejemplo que hemos visto.

https://github.com/DiscoDurodeRoer/example-web-ngxs-angular

Espero que os sea de ayuda. Si tenéis dudas, preguntad. Estamos para ayudarte.

Compartir

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *