Estructura básica de STORE con NGRX

Redux es un patrón de diseño muy ligado a la programación reactiva. Este patrón resulta especialmente útil cuando estamos desarrollando una aplicación grande que puede volverse difícil de mantener.

REDUX actúa como un contenedor del estado global de nuestra aplicación. Almacena toda la información de esta en un sólo lugar llamado store, la cual es representada bajo un objeto JavaScript accesible en todo momento.

En este artículo veremos cómo crear un proyecto angular haciendo uso de ngrx para crear una estructura básica de la store.

Principios básicos del patrón Redux

  • Fuente única de la verdad: Esto quiere decir que en la store tendremos todos los datos que necesita nuestra aplicación. Normalmente, en una aplicación Angular tenemos varios servicios para mantener los datos, cuando la aplicación crece puede hacerse difícil conocer qué servicio es el que está realizando determinados cambios. Tener un contenedor siempre disponible con todos los datos de la aplicación es muy útil en este sentido.
  • El estado es inmutable: La store es de solo lectura, nunca debe cambiarse el estado de la aplicación de forma directa, de esto se encargan las acciones. Las acciones describen la operación que están realizando (leer, modificar, borrar o añadir datos). Esto permite conocer quién ha realizado cualquier cambio en la store, lo cual, es muy útil para debuggear.
  • Los cambios se realizan con funciones puras: Las acciones describen la operación que se va a realizar en la store, pero las encargadas de realizar el cambio son funciones puras llamados reducers. Estas funciones reciben el estado anterior y una acción y devuelven un nuevo estado.

Estructura básica de STORE con NGRX: proyecto simple

Vamos a crear un proyecto simple que sea capaz de crear nuevas tareas en la store y, posteriormente, mostrarlas.

1 . Creación del proyecto e instalación de dependencias

En primer lugar creamos un nuevo proyecto angular:

La librería que se usa en angular para poder gestionar la store es ngrx:

A continuación creamos el modelo para nuestras tasks:

export interface Task {
    name: string;
    state: string;
}

 

2. Actions

Como hemos comentado antes, las acciones describen la acción que se va a realizar. Además las acciones pueden recibir un parámetro payload.

Dentro del directorio /src/app creamos el directorio store. Aquí tendremos todos los archivos que van a gestionar la store de nuestra aplicación. Las acciones las definiremos en el archivo tasks.actions.ts.

Este sería el código de nuestras acciones:

// 1 - Importaciones
import { Action } from '@ngrx/store'
import { Task } from './../tasks/task.model'

// 2 - Definición del tipo de acción
export const ADD_TASK = 'Add task'

// 3 - Creación de la clase tipo AddTask
export class AddTask implements Action {
  readonly type = ADD_TASK
  constructor(public payload: Task) { }
}

// 4 - Exportación de la acción
export type Actions = AddTask

En el primer punto importamos el modelo el modelo Action de la librería ngrx y nuestro modelo Task.

En el segundo punto estamos definiendo el tipo de acción el cuál contiene una descripción de la operación que va a realizar.

En el tercer punto creamos la clase del tipo de acción que hemos creado. Estas clases pueden recibir un payload de forma opcional.

Y, por último, exportamos las acciones para que puedan ser usadas desde los Reducer.

3. Reducer

A continuación vamos a crear el reducer en /app/src/store/tasks.reducer.ts:

// 1 - Importaciones
import { Task } from './../tasks/task.model'
import * as TaskActions from './tasks.actions'

// 2 - Estado inicial
const initialState: Task = {
  name: 'First Task',
  state: 'Pending'
}

// 3 - Switch con las funciones puras
export function taskReducer(state: Task[] = [initialState], action: TaskActions.Actions) {
  switch (action.type) {
    case TaskActions.ADD_TASK:
      return [...state, action.payload];
    default:
      return state;
  }
}

En el primer punto, además del modelo Task, importamos las acciones que hemos definido antes.

En el segundo punto definimos un estado inicial para esta parte de la store. Este estado inicial no es obligatorio.

En la última sección definimos un switch que va a evaluar el tipo de acción de entrada. El reducer ejecutará la función asociada tipo de acción. En este caso la acción ADD_TASK recibe el estado actual de la aplicación y un payload con los datos de la tarea a añadir y lo que está haciendo es devolver un nuevo estado con la acción añadida.

4. state

En el directorio /src/app creamos el archivo app.state.ts. Aquí definimos el estado de la aplicación y contendrá todos los modelos que queramos que sean accesibles. En este caso añadimos el modelo Task:

import { Task } from './tasks/task.model';

export interface AppState {
  readonly tasks: Task[];
}

5. Actualizamos app.module.ts

Debemos importar tanto la store como nuestro reducer:

import { StoreModule } from '@ngrx/store';
import { taskReducer } from './store/tasks.reducer';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    StoreModule.forRoot({tasks: taskReducer}),
  ],
  providers: [],
  bootstrap: [AppComponent]
})

6. Componentes

Llegados a este punto ya estamos en disposición de hacer uso de la store en nuestros componentes. Para ello vamos a crear dos componentes: uno que se encargará de crear nuevas tareas y otro de mostarlas.

Alta de una nueva tarea:

Así quedaría nuestro componente /src/app/tasks/add-task/add-task.component.ts:

// Importaciones
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { AppState } from './../../app.state';
import * as TaskActions from './../../store/tasks.actions';

@Component({
  selector: 'app-add-task',
  templateUrl: './add-task.component.html',
  styleUrls: ['./add-task.component.css']
})
export class AddTaskComponent implements OnInit {
  
  constructor(
    private store: Store
  ) { }

  ngOnInit() {
  }

  // Disparamos la acción
  addTask(name, state) {
    this.store.dispatch(new TaskActions.AddTask({name: name, state: state}) )
  }
}

Aquí creamos la función addTask que se va a encargar de disparar la acción AddTask de nuestra store.

En el archivo /src/app/tasks/add-task/add-task.component.html creamos un sencillo formulario que llame a la función de componente:

<div class="left">
<input name="" type="text" placeholder="Nombre" />
<input type="text" placeholder="Estado" />
<button name="" value="">Crear Tarea</button>
</div>

Leer las tareas de la store:

En el componente /src/app/tasks/list-tasks/list-tasks.component.ts vamos a leer el estado de nuestra store:

import { Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs';
import { Store } from '@ngrx/store';
import { Task } from './../task.model';
import { AppState } from './../../app.state';

@Component({
  selector: 'app-list-tasks',
  templateUrl: './list-tasks.component.html',
  styleUrls: ['./list-tasks.component.css']
})
export class ListTasksComponent implements OnInit {

  // Definimos el observable:
  tasks: Observable<task[]>;
  constructor(
    private store: Store
  ) { 
    // Accedemos a la store:
    this.tasks = this.store.select('tasks');
  }

  ngOnInit() {
  }
}

Simplemente estamos creando un observable que estamos alimentando con el objeto ‘tasks’ de la store. Ahora solo tenemos que mostrarlo en el archivo /src/app/tasks/list-tasks/list-tasks.component.html.

 

Tareas:

<ul>
<li style="list-style-type: none;">
<ul>
<li><h3>{{ task.name }}</h3></li>
</ul>
</li>
</ul>

 

Para probar todo esto solo tenemos que añadir los componentes que hemos creado en el archivo /app/src/app.component.html.

Este sería el resultado:

 

Instalando las DevTools

Una de las grandes ventajas del patrón REDUX es la facilidad de debuguear las aplicaciones, el equipo de ngrx ha creado una librería y una extensión para Google Chrome que hace mucho más sencillo el desarrollo y las pruebas de nuestras aplicaciones angular.

Para hacer uso de ella en primer lugar tenemos que instalar la librería:

A continuación debemos importarla en el archivo app.module.ts añadiendo la librería e importando el módulo:

import { StoreDevtoolsModule } from ‘@ngrx/store-devtools';

imports: [
    StoreDevtoolsModule.instrument({
      maxAge: 25, // Retains last 25 states
      logOnly: environment.production, // Restrict extension to log-only mode
    }),

Tras esto podemos añadir la extensión del navegador desde este enlace.

Lo cierto es que esta extensión es realmente potente ya que nos permite tanto ver el estado actual de la store como un histórico de los cambios realizados en ésta:

 

El código del ejemplo está disponible en este repositorio.

José Antonio Ruiz Santiago

Full-stack Developer en DIGITAL55