NGXS en Angular 6

2
5715

Índice de contenidos

1. Introducción

En este tutorial aprenderemos las bases de NGXS. Una nueva librería de manejo de estados para Angular. Responderemos a algunas preguntas como:

  • ¿Por qué otra librería de manejo de estados?
  • El flujo de datos en NGXS
  • Creación de acciones
  • Creación de estados
  • Uso desde componentes
  • Uso de un servicio externo

2. Entorno

El tutorial está escrito usando el siguiente entorno:

  • Hardware: MacBook Pro 15’ (2,5 GHz Intel Core i7, 16GB DDR3)
  • Sistema operativo: macOS Sierra 10.12.6
  • Entorno de desarrollo: Visual Studio Code
  • Versión de Angular: 6.1.8
  • Versión de Angular CLI: 6.2.3
  • Versión de @ngxs/store: 3.2.0

3. ¡¿Por qué otra librería de estados?!

El objetivo de NGXS es hacer cosas lo más simple y accesible posible.

Si has usado ngrx o Redux, sabrás la cantidad de boilerplate que estas librerías requieren. Uno de los objetivos principales de NGXS es reducir el boilerplate. O como ell@s dicen:

Do more things with less

Curva de dificultad reducida

Una de las razones por la que la curva de aprendizaje se dificulta con ngrx o Redux, (especialmente para desarrolladores que vienen de Angular o de OOP) es aprender el nuevo paradigma funcional.

NGXS ofrece una forma de trabajo más parecida a Angular.

No más bloques de switch

La librería se encarga de saber a que método llamar según qué acción es disparada.

Inyección de dependencias

Una de las mejores cosas de Angular es su dependency injection. NGXS permite inyectar servicios de Angular desde las propias clases de estado.

Ciclo de vida de las acciones

En NGXS, las acciones son asíncronas, lo que permite que tengan un ciclo de vida. Esto significa que podemos esperar a que una o varias acciones sean completadas. Facilitando así los flujos de trabajo.

Promesas

Los observables de Rxjs son muy útiles, pero en ciertos casos una promesa de ES6 es la opción a elegir. NGXS permite el uso de promesas tanto como el de Observables.

4. El flujo de datos en NGXS

El flujo de datos en NGXS suele ser el siguiente:

Flujo de datos de NGXS

  1. El componente invoca una acción con dispatch.
  2. El estado recibe la acción y muta sus datos.
  3. El componente es notificado del cambio y es actualizado automáticamente.

Esto significa que:

Siempre que ocurra una acción, el estado será mutado.

Una acción conlleva un estado

Esta garantía nos permite delegar el control de estado en NGXS y centrarnos en el desarrollo.

5. Instalación

NGXS se añade como una dependencia a nuestro proyecto de Angular con:

  npm install @ngxs/store
  # o
  yarn add @ngxs/store

Recomiendo instalar también las dependencias de desarrollo con:

  npm install @ngxs/store
  npm install @ngxs/logger-plugin
  npm install @ngxs/devtools-plugin
  # o
  yarn add @ngxs/store
  yarn add @ngxs/logger-plugin
  yarn add @ngxs/devtools-plugin

Para añadirlo a nuestro módulo de aplicación importamos las dependencias en app.module.ts:

  import { NgxsModule } from '@ngxs/store';

  import { NgxsReduxDevtoolsPluginModule } from '@ngxs/devtools-plugin';
  import { NgxsLoggerPluginModule } from '@ngxs/logger-plugin';
  ​
  @NgModule({
    imports: [
      NgxsModule.forRoot([
        ZooState
      ]),
      NgxsReduxDevtoolsPluginModule.forRoot(),
      NgxsLoggerPluginModule.forRoot(),
    ]
  })
  export class AppModule {}

Las librerías NgxsReduxDevtoolsPluginModule y NgxsLoggerPluginModule nos permiten ver el estado de la aplicación en tiempo real en la consola del navegador. Te recomiendo que durante este ejercicio tengas los devtools abiertos y que vayas viendo los cambios.

También puedes añadir a tu navegador la extensión Redux para Chrome
y para Firefox

6. Especificación de la aplicación.

Si has leído mi último tutorial, sabrás que antes de ponerme a programar me gusta establecer una especificación clara de la necesidades del proyecto. Vamos a hacer una aplicación muy simple, pero siempre está bien dejar claro lo que vamos y no vamos a hacer desde el principio.

Vamos a desarrollar una aplicación de contador:

  • Mostrará un número entero, nuestro total. Por defecto 0.
  • Tendrá un botón [-], si se pulsa, restará 1 a nuestro total.
  • Tendrá un botón [+], si se pulsa, sumará 1 a nuestro total.
  • Tendrá un botón [R], si se pulsa, reseteará el total a 0.

Y no podría faltar el mockup de la interfaz.

Mockup de la Interfaz

7. Creación de una Acción.

Nuestra aplicación tiene tres acciones posibles:

  • Sumar 1 al total.
  • Restar 1 al total.
  • Resetear el total a 0.

Vamos a crear el fichero store/counter.actions.ts y a añadir las tres acciones.

  export class Increment {
    static readonly type = '[Counter] Increment';
    constructor() {}
  }

  export class Decrement {
    static readonly type = '[Counter] Decrement';
    constructor() {}
  }

  export class SetTotal {
    static readonly type = '[Counter] Set Total';
    constructor(public number: number) {}
  }

Una acción tiene:

  • Un type. El «nombre» de la acción, que lo representa.
  • Un constructor. Por el que podemos pasar todos los argumentos que necesitemos.

8. Creación de un Estado.

Ahora que tenemos nuestras acciones, creamos el archivo store/counter.state.ts y añadimos:

    import { State, Store, StateContext, Action } from '@ngxs/store';

    import { Increment, Decrement, SetTotal } from './counter.actions';

    // Creamos un tipo para nuestro estado.
    export interface CounterStateModel {
      total: number;
    }

    // Creamos nuestro estado con la anotación @State
    // Le damos el tipo al estado.
    // Le damos nombre al 'slice' o partición del estado.
    // Damos valor por defecto al estado.
    @State({
      name: 'counter',
      defaults: {
        total: 0
      }
    })
    export class CounterState {
      // Inyectamos la store global en nuestro estado.
      constructor(private store: Store) {}

      // Relacionamos la acción con su implementación con la anotación @Action(nombre_de_acción).
      // Inyectamos a la función el estado actual con 'stateContext: StateContext'.
      @Action(Increment)
      Increment(stateContext: StateContext) {
        // Recogemos el valor actual del total con 'store.getState().nombre_propiedad'.
        const currentTotal = stateContext.getState().total;
        // Actualizamos el estado con pathState({nombre_propiedad: valor}).
        stateContext.patchState({ total: currentTotal + 1 });
      }

      @Action(Decrement)
      Decrement(stateContext: StateContext) {
        const currentTotal = stateContext.getState().total;
        stateContext.patchState({ total: currentTotal - 1 });
      }

      // Inyectamos el valor de la acción y recuperamos el valor pasado por parámetro.
      @Action(SetTotal)
      SetTotal(stateContext: StateContext, action: SetTotal) {
        stateContext.patchState({ total: action.value });
      }
    }

Dentro de un estado definimos el comportamiento que tendrán las acciones al ser invocadas con ‘@Action(nombre_de_acción)’.

También podemos usar ‘@Selector()’ para acceder de forma directa a datos del estado desde otra parte de la aplicación. Es como una query predefinida, un «acceso directo» al estado.

No te olvides de añadir el estado que acabamos de crear en el array NgxsModule.forRoot([]) de AppModule:

//...
import { CounterState } from './store/counter.state';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    NgxsModule.forRoot([CounterState], { developmentMode: true }), // <--
    NgxsReduxDevtoolsPluginModule.forRoot(),
    NgxsLoggerPluginModule.forRoot()
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule {}

9. Creación de un Componente.

Ahora vamos a crear un componente para probar nuestras acciones y estado. (En este caso lo haremos en el AppComponent principal.

<section class="counter">
  <button (click)="decrement()">-</button>
  <p>{{ (counter$ | async)?.total }}</p>
  <button (click)="increment()">+</button>
  <button (click)="reset()">R</button>
</section>
import { Component, OnInit } from '@angular/core';
import { Store, Select } from '@ngxs/store';
import { Increment, Decrement, SetTotal } from './store/counter.actions';
import { Observable } from 'rxjs';
import { CounterStateModel, CounterState } from './store/counter.state';
import { UsersRequestAttempt } from './store/users.actions';
import { UsersStateModel } from './store/users.state';

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent implements OnInit {
  // Seleccionamos el 'slice' counter del estado global.
  @Select(state => state.counter)
  counter$: Observable<CounterStateModel>;

  // Inyectamos la store global en el componente.
  constructor(private store: Store) {}

  // Invocamos a las acciones del store.
  increment() {
    this.store.dispatch(new Increment());
  }

  decrement() {
    this.store.dispatch(new Decrement());
  }

  reset() {
    this.store.dispatch(new SetTotal(0));
  }
}

Debemos destacar que @Select() nos devuelve un observable de tipo CounterStateModel. Por lo que en el template HTML debemos usar el pipe async para que Angular se encargue de manejar el renderizado de datos asíncrono.

10. Guardando datos de un Servicio Externo.

Lo que sabemos hasta ahora está muy bien, pero en el mundo real, la mayoría de datos del estado de nuestra aplicación vendrán de servicios desde nuestro servidor. Las peticiones HTTP añaden un nivel de complejidad al sistema, ya que tenemos que controlar los errores que puedan surgir por el camino.

En este ejercicio vamos a usar una API REST de usuarios falsos.

Si nos metemos al endpoint de la API podemos ver lo que nos devuelve:

https://jsonplaceholder.typicode.com/users

[
{
  "id": 1,
  "name": "Leanne Graham",
  "username": "Bret",
  "email": "Sincere@april.biz",
  "address": {
    "street": "Kulas Light",
    "suite": "Apt. 556",
    "city": "Gwenborough",
    "zipcode": "92998-3874",
    "geo": {
      "lat": "-37.3159",
      "lng": "81.1496"
    }
  },
  "phone": "1-770-736-8031 x56442",
  "website": "hildegard.org",
  "company": {
    "name": "Romaguera-Crona",
    "catchPhrase": "Multi-layered client-server neural-net",
    "bs": "harness real-time e-markets"
  }
},
...]

Sabiendo lo que devuelve, vamos a hacer que muestre los usuarios de una forma como esta:

Antes de crear el servicio, vamos a crear el tipo user en el archivo models/user.model.ts. No es necesario que contenga todos los atributos que nos devuelve la API.

export interface User {
  id: number;
  name: string;
  username: string;
  email: string;
  website: string;
}

Creamos el archivo services/users.service.ts e implementamos la lógica necesaria:

import { Injectable } from '@angular/core';
import { HttpClientModule } from '@angular/common/http';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { Observable } from 'rxjs';
import { User } from '../models/user.model';

@Injectable({
  providedIn: 'root'
})
export class UsersService {
  private endpoint = 'https://jsonplaceholder.typicode.com/users';

  constructor(private http: HttpClient) {}

  getUsers(): Observable<User[]> {
    return this.http.get<User[]>(this.endpoint);
  }
}

No nos olvidemos de importar el HttpClientModule en AppModule:

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    NgxsModule.forRoot([CounterState], { developmentMode: true }),
    NgxsReduxDevtoolsPluginModule.forRoot(),
    NgxsLoggerPluginModule.forRoot(),
    HttpClientModule
  ]...})

Creamos el archivo store/users.actions.ts y añadimos las acciones que vamos a usar.

import { HttpErrorResponse } from '@angular/common/http';
import { User } from '../models/user.model';

export class UsersRequestAttempt {
  static readonly type = '[User] Request Attempt';
  constructor() {}
}

export class UsersRequestSuccess {
  static readonly type = '[User] Request Success';
  constructor(public users: User[]) {}
}

export class UsersRequestFailure {
  static readonly type = '[User] Request Failure';
  constructor(public error: HttpErrorResponse) {}
}

Creamos el archivo store/users.state.ts e implementamos las acciones que acabamos de añadir.

import { User } from '../models/user.model';
import { State, Store, Action, StateContext } from '@ngxs/store';
import {
  UsersRequestAttempt,
  UsersRequestSuccess,
  UsersRequestFailure
} from './users.actions';
import { UsersService } from '../services/users.service';
import { catchError } from 'rxjs/operators';
import { throwError } from 'rxjs';

export interface UsersStateModel {
  users: User[];
}

@State({
  name: 'users',
  defaults: {
    users: []
  }
})
export class UsersState {
  constructor(private store: Store, private usersService: UsersService) {}

  @Action(UsersRequestAttempt)
  async UsersRequestAttempt() {
    this.usersService.getUsers().subscribe(
      data => {
        this.store.dispatch(new UsersRequestSuccess(data));
      },
      error => {
        this.store.dispatch(new UsersRequestFailure(error));
      }
    );
  }

  @Action(UsersRequestSuccess)
  UsersRequestSuccess(
    stateContext: StateContext,
    action: UsersRequestSuccess
  ) {
    stateContext.patchState({ users: action.users });
  }

  @Action(UsersRequestFailure)
  UsersRequestFailure(
    stateContext: StateContext,
    action: UsersRequestFailure
  ) {
    console.error('Failed to get Users. Try again later', action.error);
  }
}

No nos olvidemos de añadir el nuevo estado en AppModule NgxsModule.forRoot([]):

import { HttpClientModule } from '@angular/common/http';

@NgModule({
  declarations: [AppComponent],
  imports: [
    BrowserModule,
    NgxsModule.forRoot([CounterState, UsersState], { developmentMode: true }),
    NgxsReduxDevtoolsPluginModule.forRoot(),
    NgxsLoggerPluginModule.forRoot(),
    HttpClientModule
  ]...})

Para probar el funcionamiento, vamos a añadir a app.component.ts lo siguiente:

export class AppComponent implements OnInit {
  ...
  @Select(state => state.users)
  users$: Observable;
  ...
  ngOnInit(): void {
    this.store.dispatch(new UsersRequestAttempt());
  }
}

Y añadimos esto en app.component.html:

<section class="users">
  <div *ngFor="let user of ( users$|async )?.users" class="user">
    <span>{{ user.id }}</span>
    <span>{{ user.name }}</span>
    <span>{{ user.email }}</span>
  </div>
</section>

Muy bien, ahora si entras en la página, deberías ver una lista con 10 usuarios.

11. Conclusiones.

NGXS es una librería muy nueva, los primeros commits comenzaron en febrero de 2018, tiene 8 meses de edad en el momento de publicación del tutorial.

Es difícil saber si superará a ngrx como librería de estados de Angular por defecto o si continuará el mantenimiento por la comunidad. Pero actualmente, se trata de un buen concepto para una futura alternativa.

Puedes clonar y probar el proyecto con:

git clone https://github.com/ArturoRodriguezRomero/ngxs-tutorial

cd ngxs-tutorial

npm install

ng serve

O descargarlo desde Github:

Proyecto en Github

12. Referencias.

2 COMENTARIOS

DEJA UNA RESPUESTA

Por favor ingrese su comentario!

He leído y acepto la política de privacidad

Por favor ingrese su nombre aquí

Información básica acerca de la protección de datos

  • Responsable:
  • Finalidad:
  • Legitimación:
  • Destinatarios:
  • Derechos:
  • Más información: Puedes ampliar información acerca de la protección de datos en el siguiente enlace:política de privacidad