Índice de contenidos
1. Entorno
Este tutorial está escrito usando el siguiente entorno:
- Hardware: Slimbook Pro 2 13.3″ (Intel Core i7, 32GB RAM)
- Sistema Operativo: LUbuntu 18.04
- Angular 6
- Akita 1.9.1
2. Introducción
Actualmente lo más complicado de gestionar en una aplicación completamente front hecha con Angular, Vue, ReactJS… es la gestión del estado. Es decir, mantener cierta información de la aplicación durante todo el ciclo de vida de la aplicación y que todos los componentes tengan acceso puntual a esta información. Podemos distinguir dos tipos de estado: de dominio, que representan el estado del servidor, por ejemplo, los valores de una determinada entidad y el estado de interfaz, por ejemplo, si un determinado combo tiene que aparecer desplegado o cuál es la pestaña que está activa.
Cuando la aplicación no es grande y la información a almacenar no es sensible podemos hacer uso del LocalStorage para mantener el estado, pero cuando la aplicación se hace cada vez más grande está solución no es viable.
Dentro del ecosistema de Angular ya hablamos en su momento del Angular Model Pattern pero también se hace pesado para aplicaciones realmente grandes, por lo que en estos casos se recomienda pasar directamente a NgRx.
El problema con NgRx es que estamos más acostumbrados a la programación orientada a objetos que a la programación funcional, y este cambio de paradigma a muchos se nos hace muy cuesta arriba y hace que tengamos que aprender un framework que muchas veces no parece Angular.
Pues bien ahora tenemos una nueva opción antes de NgRx para aplicaciones grandes, Akita que pretende hacer la gestión del estado de aplicaciones grandes desde un enfoque de programación orientada a objetos, como muestra su arquitectura.
En contraposición al siguiente esquema de NgRx donde vemos que todo pasa por un proceso funcional de reducer con funciones puras que actualizan el estado creando un estado nuevo cada vez.
Ambas soluciones se implementan sobre las funcionalidades de la programación reactiva (RxJS)
Akita maneja 4 conceptos principales:
- Store: es la «fuente de la verdad» del estado de nuestra aplicación. Es el elemento al que se va a consultar y modificar el estado de la aplicación. Se podría asemejar con una base de datos.
- Model: es la representación del store. Contiene todos los objetos con sus atributos que queremos almacenar en el estado de la aplicación. Se podría asemejar con las tablas de una base de datos.
- Service: es recomendable crear servicios de creación, actualización y borrado de los elementos del estado, en vez de trabajar directamente con el Store.
- Query: son clases que tienen la responsabilidad de consultar el Store. Las podemos considerar como las queries que lanzamos contra una base de datos.
3. Vamos al lío
La instalación se hace de la forma habitual:
$> npm install @datorama/akita --save
Para este ejemplo vamos a hacer una aplicación con dos componentes: el primero «set-name» se va a encargar de establecer un nombre en el estado de la aplicación; y el segundo «get-name» se va encargar de consultar esa parte del estado para mostrarlo actualizado por pantalla.
Vamos a crear el estado de la aplicación, para estructurarlo un poco vamos a crear la carpeta «state» dentro de «app».
Dentro de la carpeta «state» vamos a crear el fichero name.store.ts con el siguiente contenido:
import { Injectable } from '@angular/core'; import { Store, StoreConfig } from '@datorama/akita'; export interface NameState { name: string | null; } export function createInitialName(): NameState { return {name: ''}; } @Injectable({ providedIn: 'root' }) @StoreConfig({ name: 'name' }) export class NameStore extends Store{ constructor() { super(createInitialNameState()); } setName(name: string) { this.update({name}); } resetName() { this.update(createInitialNameState()); } }
En esta clase definimos el «Model» de nuestro estado, que solo va a ser un objeto con un atributo «name» de tipo string y creamos el «Store» asociado extendiendo de la clase «Store» de Akita con el tipo definido para el «Model». Además definimos dos métodos para cambiar el estado: setName para establecer un nuevo nombre y resetName para volver al nombre inicial que es cadena vacía.
El siguiente paso sería crear el servicio para acceder a la funcionalidad del Store sin hacerlo directamente, en esta clase pondríamos las llamadas a un servicio proxy de comunicación con el servidor para la recuperación y actualización de datos. Para nuestro caso, este podría ser un posible contenido:
import { Injectable } from '@angular/core'; import { NameStore } from './name.store'; @Injectable({ providedIn: 'root' }) export class NameService { constructor(private nameStore: nameStore) { } setName(name: string) { this.nameStore.setName(name); } resetName() { this.nameStore.resetName(); } }
Ahora necesitamos una clase que nos proporcione acceso de lectura al «Store». Para ello creamos una clase que extiende de Query pasándole el tipo NameState. Este sería el contenido:
import { Injectable } from '@angular/core'; import { Query } from '@datorama/akita'; import { NameState, NameStore } from './name.store'; @Injectable({ providedIn: 'root' }) export class NameQuery extends Query{ getName$ = this.select(state => state.name); constructor(protected store: NameStore) { super(store); } }
La clase define el atributo getName$ de tipo Observable de string, que va a ser al que tenemos que suscribirnos para recibir la actualización del nombre en el estado de la aplicación.
Ahora solo resta crear dos componentes: uno set-name que se va a encargar de establecer el nombre en el estado de la aplicación y otro get-name que se va a encargar de leer el nombre almacenado en el estado. Para crearlos hacemos uso del CLI de Angular de esta forma:
$> npm run ng -- generate component set-name $> npm run ng -- generate component get-name
Primero vamos a implementar el componente set-name. Para ello editamos el fichero set-name.component.ts y haciendo uso del servicio creamos métodos para poder establecer y resetear el nombre. Este sería el contenido:
import { Component, OnInit } from '@angular/core'; import { NameService } from '../state/name.service'; @Component({ selector: 'app-set-name', templateUrl: './set-name.component.html', styleUrls: ['./set-name.component.css'] }) export class SetNameComponent implements OnInit { constructor( private nameService: NameService ) { } ngOnInit() { } setName(user: string) { this.nameService.setName(user); } resetName() { this.nameService.resetName(); } }
En el template asociado (set-name.component.html) implementamos un campo de texto con dos botones: uno con la funcionalidad de establecer el nombre que pongamos en el campo de texto y otro para resetear el valor del componente. Este sería el contenido:
Name:
Ahora vamos a implementar el componente get-name, encargado de mostrar por pantalla el nombre que tenemos en el estado de la aplicación y si cambia, automáticamente mostrar el cambio. Para ello editamos el fichero get-name.component.ts donde haciendo uso del servicio «NameQuery» obtenemos el valor del nombre. Este sería el contenido:
import { Component, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; import { NameQuery } from '../state/name.query'; @Component({ selector: 'app-get-name', templateUrl: './get-name.component.html', styleUrls: ['./get-name.component.css'] }) export class GetNameComponent implements OnInit { name$: Observable; constructor( private nameQuery: NameQuery ) { } ngOnInit() { this.name$ = this.nameQuery.getName$; } }
Y en el template asociado hacemos uso del pipe async para suscribirnos y desuscribirnos automáticamente al observable y mostrar el valor. Este sería el contenido:
Name: {{name$ | async}}
De esta forma colocando los dos componentes por pantalla, vemos que al poner el nombre en el campo de texto y pulsar en «Set Name» el otro componente muestra el valor introducido de forma automática.
Pero ahora qué pasa si refrescamos el navegador, entonces todo el estado se pierde y dejamos de mostrar el nombre. Para evitar esto, Akita tiene el método persistState() que nos permite persistir el estado de la aplicación en el LocalStorage, de forma que sin hacer nada, al refrescar el navegador vemos que el nombre se sigue mostrando. El lugar indicado para llamar a la función persistState() es el fichero main.ts de esta forma:
import { enableProdMode } from '@angular/core'; import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; import { persistState } from '@datorama/akita'; import { AppModule } from './app/app.module'; import { environment } from './environments/environment'; if (environment.production) { enableProdMode(); } persistState(); platformBrowserDynamic().bootstrapModule(AppModule) .catch(err => console.log(err));
Nota: cuidado con esta función ya que todo el estado se mostrará en claro en el LocalStorage; así que nada de almacenar claves propias en el estado y hacer uso de esta función 😉
A poco que hayas visto algo sobre NgRx o Redux te habrá llamado la atención el plugin de Chrome que te permite navegar por los cambios del estado. Pues esto lo podemos conseguir en Akita simplemente instalando esta dependencia.
$> npm install @datorama/akita-ngdevtools --save-dev
Y estableciendo esta forma de importar el módulo en el módulo principal de la aplicación a fin de que solo esté disponible cuando estemos en modo de desarrollo.
import { AkitaNgDevtools } from '@datorama/akita-ngdevtools'; @NgModule({ imports: [environment.production ? [] : AkitaNgDevtools.forRoot()] bootstrap: [AppComponent] }) export class AppModule {}
4. Conclusiones
En este tutorial hemos visto lo más básico de Akita pero ya deja ver que es una forma mucho
más «natural» de manejar el estado de nuestras grandes aplicaciones Angular.
Cualquier duda o sugerencia en la zona de comentarios.
Saludos.