Gestión de estado en aplicaciones Angular 2

Gestión de estado en aplicaciones Angular 2


Gestionar el estado de una aplicación es complicado. Necesitamos coordinación en múltiples frontales (backend), web workers y componentes IU. Patrones como Redux y Flux se diseñaron para resolver este problema haciendo la coordinación más explícita. En este artículo, te mostraremos como implementar un patrón similar con tan sólo unas pocas líneas de código, usando RxJS. Después, veremos como podemos usar este patrón en una aplicación sencilla de Angular 2.

Propiedades del núcleo (core)

Cuando hablamos acerca de un patrón de arquitectura, me gusta empezar describiendo sus propiedades del núcleo – algo que puede dibujarse en el reverso de una servilleta. La dificultad real, por supuesto, está en los detalles, y en seguida los trataremos, pero una visión a alto nivel es siempre útil.
En cierto modo, lo que vamos a construir es similar a Redux.

Estado inmutable

El estado completo de la aplicación se guarda como una estructura de datos inmutable. De modo que cada vez que ocurre un cambio, se construye una nueva instancia de la estructura de datos. Aunque esto parece que pueda limitarnos, tiene sus ventajas. Una de ellas es que puede hacer las aplicaciones de Angular bastante más rápidas.

Interacción = acción

El único modo de actualizar el estado de una aplicación es emitir una acción. Como resultado, la mayoría de las interacciones de los usuarios emiten acciones. Y, si queremos simular una interacción, tan sólo necesitamos emitir la secuencia correcta de acciones.

fn(a:Observable ):Observable

La lógica de la aplicación viene expresada como una función que mapea un Observable de acciones en un Observable de estados de aplicación.

La función se invoca una sola vez. Esto es diferente de Redux, donde dicha función se invoca en cada acción.

Aplicación y Límites de vista

La lógica de aplicación y de la vista están completamente separadas. El objeto ‘dispatcher’ y estado son los límites a través de los que la aplicación y la vista se comunican. La vista emite acciones usando el dispatcher y escucha los cambios de estado.

Ejemplo

Como este patrón es similar a Redux, podemos sacar provecho del excelente curso en vídeo de Dan Abaramov acerca de este tema. En este curso, Dan nos muestra como construir una aplicación de tareas pendientes usando Redux. Para facilitar la comparación entre la implementación de Redux y nuestra implementación, construiremos la misma aplicación en este artículo.
Puedes trastear con el ejemplo que construiremos aquí: Plunk [enlace roto]

Estado de aplicación

Podemos tener una idea de que lo que hace la aplicación mirando las definiciones de tipos de estados y su listade de acciones. Así que empezamos con eso

El estado de nuestra aplicación es sólo un array de tareas pendientes (todos) y un filtro definiendo cuales debe mostrar.

Acciones

Nuestra aplicación soportará las siguientes acciones: AddTodoAction, ToggleTodoAction y SetVisibilityFilter.

El tipo Action, el cual es una unión de estas tres acciones, representa todo lo que la aplicación será capaz de hacer.

Observable

Necesitamos definir la función de estado que retornará un Observable de aplicaciones de estado. Para hacer el ejemplo un poco más real, separaré la función de estado en dos partes: una tratará con las tareas pendientes y otra con el filtro de visibilidad.
Empezamos con las tareas pendientes

Aquí tenemos varias cosas a destacar.
Primero, observamos la firma la función. La función no toma una acción, sino un Observable RxJS de acciones. Del mismo modo, no devuelve una lista de tareas, sino un Observable.
Segundo, actions.scan aplica la función para acumular sobre una secuencia Observable y devuelve cada resultado intermedio. Así, se emitirá una nueva lista de tareas después de cada acción.
Tercero, TypeScript se da cuenta de que la acción dentro de la cláusula ‘if’ es un AddTodoAction. Esto significa que podemos acceder a las propiedades todoId y text, pero no filtrar. Esto permite escribir estas funciones sin que se confundan los tipos. Esto es muy bueno porque en estas funciones reside la ‘inteligencia’ de nuestra aplicación y como resultado, dejan de ser triviales. Así pues, tener la ayuda de un compilador es de gran importancia.
Ahora, extendamos las tareas todos añadiendo la habilidad de activar/desactivar una tarea.

Del mismo modo que los todos, podemos implementar una función que cree un Observable de filtro de visibilidad.

Y finalmente, podemos combinarlos crea stateFn

Muchas cosas suceden en estas seis líneas de código.
Primero, creamos las tareas y filtramos Observables usando las funciones definidas arriba. Después, las metemos en un Observable de pares, el cual mapeamos a un Observable de AppState
Hay un problema con este observable. Si un componente se suscribe a él, el componente no recibirá datos hasta que el Observable emita un nuevo evento. Para que esto suceda, se debe emitir una nueva acción. Esto no es lo que queremos. Lo que queremos es que el componente reciba la última instantánea en el momento que se suscribe. Y para esto esta BehaviourSubject.

Un Sujeto de Comportamiento (Behaviour Subject) es un Observable que emitirá el último valor a un nuevo suscriptor.
Para entender mejor como funciona stateFn, escribimos un tests unitarios.

Si estás familiarizado con Redux, verás que la función stateFn es similar a un reductor (reducer) de Redux. Pero en realidad hay una gran diferencia: la función stateFn sólo se invoca una vez, mientras que el reductor de Redux se invoca cada acción.
Esto es importante por las siguientes razones:

Sólo un Observable

La función stateFn se llama una sola vez para crear el estado Observable. El resto de la aplicación (componentes de Angular 2) no tienen porque saber no que stateFn existe. Sólo les importa el Observable. Esto nos da mucha flexibilidad acerca de como implementar la función. En este ejemplo, lo hicimos parecido a Redux. Pero podemos cambiarlo sin modificar nada más en la aplicación. Además, dado que Angular 2 ya trae RxJS, no tenemos que importar nuevas librerías.

Síncrono y Asíncrono

En este ejemplo, stateFn es síncrono. Pero como los Observables están basados en push, podemos hacerlo asíncrono sin cambiar la API pública de la función. Así, podemos hacer algunos manejadores de acciones síncronos y otros asíncronos sin afectar a ningún componente. Esto es importante a medida que crece la aplicación, cuando más y más acciones deban realizarse en el servidor o web worker. Una desventaja de usar colecciones basadas en push es que son difíciles de usar, pero Angular 2 proporciona una primitiva (la tubería asíncrona -async pipe-) para ayudarnos con esto.

El poder de RxJS

RxJS proporciona muchos combinadores (combinators) potentes, los cuales permiten implementar interacciones complejas en una sencilla forma declarativa. Por ejemplo, cuando la acción A se emite, la aplicación debería esperar a la acción B y entonces emitir un nuevo estado. Pero si la acción B no se emite en cinco segundos, la aplicación debería emitir un estado error. Esto se puede implementar con sólo unas pocas líneas de código usando RxJS.
Esto es clave. La complejidad de la gestión del estado de la aplicación viene dada por el hecho de tener que coordinar estas interacciones. Una herramienta poderosa como RxJS, que se encarga de gran parte de la lógica, puede disminuir enormemente la complejidad de la gestión de estados.

Aplicación y Límites de vista

En este punto, aún no hemos escrito código específico de Angular (no hemos escrito ningún componente). Este es uno de los beneficios de esta arquitectura, la lógica de aplicacón y de la vista están separadas. Pero, ¿cómo se comunican?
Lo hacen a través de los objetos estado y dispatcher.

Los podemos crear de la siguiente manera

  • dispatcher es un sujeto RxJS, lo que significa que es a la vez un Observable y un Observador. Así, podemos pasarlo en stateFn y usuarlo para emitir acciones.
  • es un observable devuelto por la función stateFn

Podemos registrar los proveedores así:

Y entonces inyectarlos en los componentes.

Sin almacén

Fíjate que al contrario que en Redux o Flux, no hay almacén. El dispatcher es sólo un Observador RxJS, y el estado es sólo un Observable. Esto significa que podemos usar los combinadores incorporados de RxJS para cambiar el comportamiento de estos objetos, proporcionar simulaciones (mocks), etc.

Sin objetos globales

Como usamos inyección de dependencias para inyectar el estado y el dispatcher, y estos son dos objetos separados, podemos decorarlos fácilmente. Por ejemplo, podemos anular (override) el proveedor dispatcher en un componente sub-árbol para registrar todas las acciones emitidas sólo para ese sub-árbol. O podemos envolver el dispatcher para que abarque automáticamente todas las acciones, lo que puede ser muy útil cuando varios equipos están trabajando en la misma aplicación. También podemos decorar el estado del proveedor para, por ejemplo, activar el antirrebote (debouncing).

Vista

Finalmente, llegamos a la parte más importante, implementar la capa de la vista.

Mostrando tareas

Empezamos con un componente que muestra una única tarea.

Esto es lo que Dan Abramov llama un componente ‘tonto’ o de representación. Este componente no conoce nada de la parte de la aplicación. Tan sólo sabe mostrar una lista.
Después, el componente de listado de tareas.

La primera cosa que hacemos aquí es crear un observable de tareas filtradas usando el estado inyectado. Como los observables están basados en push, se hace extraño trabajar con ellos. El equipo de Angular es consciente de esto, por eso proporciona varías ayudas. Por ejemplo, la tubería asíncrona (async pipe) extrae el último valor de un observable. Esto nos permite usar un observable de un objeto en cualquier lugar donde ese objeto se requiera.
Segundo, usamos el dispatcher inyectado para emitir una nueva acción en el manejador de evento emitToggle.
Ese componente conoce la aplicación porque inyecta el dispatcher y el estado. Podría decirse que este componente mezcla temas de presentación y ajenos a esta. Así que podíamos plantearnos separarlo en dos. Pero, no estoy seguro de que valga la pena. Los componentes de Angular ya separan los aspectos de presentación en una plantilla. Separar cada componente en dos sería exagerado.

Añadiendo tareas

Ahora, vamos a crear el componente para añadir tareas.

Filtrando tareas

Después, creamos la posibilidad de filtrar tareas:

Componente raíz

Finalmente, añadimos el componente raíz combinando diferentes partes en nuestra aplicación.

¡Es rápido!

El patrón descrito no sólo hace que las aplicaciones de Angular sean más fáciles de organizar y refactorizar. También mejora su rendimiento. Esto es porque cuando un estado de aplicación se guarda como una estructura de datos inmutable, y los cambios en el estado se representan como un observable, podemos usar la estrategia OnPush para todos los componentes. Para conocer más acerca de esto, lee Angular, inmutabilidad y encapsulación (en inglés).

Otras maneras de manejar el estado

Naturalmente, este no es el único modo de gestionar el estado de aplicación en Angular. Por ejemplo, podemos usar @ngrx/store. También podemos escribir aplicaciones que no usan datos inmutables u observables, y en su lugar usan DCI o servicios por caso de uso.

Resumen

Coordinarse entre múltiples frontales, web workers y componentes UI hacen de la gestión del estado un dessafío. Patrones como Redux y Flux nos ayudan con esto. En este artículo, hemos mostrado los sencillo que es implementar un patrón similar con sólo unas líneas de código usando RxJS. También hemos mostrado como usarlo para implementar una aplicación Angular 2 sencilla.

Nota: puedes encontrar el artículo original en https://dzone.com/articles/managing-state-in-angular-2-applications-1

Deja un comentario

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