Cómo usar Redux en una aplicación frontend basada en Web Components y LitElement.
Redux es una librería para el control del estado que puedes usar en aplicaciones desarrolladas bajo cualquier framework frontend. Aunque realmente no estás obligado a usarlo en aplicaciones basadas en Web Components en general, encaja casa muy bien con el modelo de desarrollo que nos propone LitElement, por lo que nos parece muy relevante para abordar.
En resumen, Redux nos ofrece la posibilidad de usar un contenedor global para el estado de una aplicación, que implementa un flujo de datos de la información unidireccional. ¿Es interesante para tu aplicación basada en LitElement? eso es algo que te tienes que responder tú mismo. Como primera alternativa para arquitectura de datos en una aplicación LitElement tenemos el patrón mediador. Si se te queda pequeño y deseas ampliar tus recursos, implementar Redux es una buena idea también.
Comenzamos la implementación de Redux
Vamos a centrarnos en la parte práctica, así que iremos directamente a la implementación de Redux dentro de una aplicación LitElement.
Instalando dependencias (Redux y PWA-Helpers)
Comenzamos por instalar Redux con el comando:
npm i redux
Ahora, además de Redux, vamos a instalar una segunda herramienta, muy interesante como complemento en el desarrollo de Progressive Web Apps, llamada PWA- Helpers. Esta librería provee de varias funciones y mixins útiles para el desarrollo de aplicaciones progresivas. Entre otras muchas cosas nos ofrece un mixin llamado "connect" que nos permite conectar web components con el store de Redux.
npm i pwa-helpers
Creando el store de Redux
El primer paso, ya del lado del código, para implementar Redux es la creación del "store". El store es el contenedor global de los datos que maneja la aplicación, lo que se llama el estado. En Redux el store es el sitio donde se almacenan los datos que la aplicación debe de usar.
Tenemos que comenzar creando un store. Lo que ocurre es que, para crear el store, me facilitaría mucho disponer antes de lo que conocemos como "reducer". Un reducer no es más que una función que recibe acciones y se encarga de realizar la manipulación del estado, conforme a lo que las acciones solicitan.
Vamos a imaginar de momento que tenemos nuestro reducer ya listo (enseguida veremos su código), para resumir la creación del store en las siguientes líneas de código.
A continuación tienes el código del módulo redux/store.js
:
// importamos la función createStore, de Redux
import { createStore } from 'redux';
// importamos nuestro reducer, que enseguida veremos cómo se hace
import { reducer } from './reducers/reducer';
// creamos el store con la función createStore(), enviando el reducer como parámetro.
// exportamos el store para que otros lo puedan importar fuera de este módulo.
export const store = createStore(reducer);
Creando el reducer
Ahora vamos a crear el reducer para la aplicación. El reducer es una función, que recibe un estado y una acción. Cuando el store tenga que realizar alguna acción, simplemente llamará al reducer, indicando el estado actual y la acción que debe ser ejecutada.
En la práctica un reducer se implementa generalmente con un switch, con una rama para cada tipo de acción tengamos que procesar.
A continuación tienes el código del módulo redux/reducer/reducer.js
:
// definimos un estado inicial. Si no se tiene estado, al arrancar la aplicación, se tomarán estos valores.
const estadoInicial = {
counter: 0,
appName: 'MyApp'
}
// creamos y exportamos la función del reducer
export const reducer = (state = estadoInicial, action) => {
switch(action.type) {
case "INCREMENT":
return {
...state,
counter: state.counter + 1
}
case "DECREMENT":
return {
...state,
counter: state.counter - 1
}
case "CHANGE_APP_NAME":
return {
...state,
appName: action.name
}
default:
return state;
}
}
Como puedes ver, en nuestra aplicación tendremos un código de estado inicial, que contiene tan solo dos propiedades de ejemplo:
{
counter: 0,
appName: 'MyApp'
}
Luego, en la función del reducer, estamos tratando diferentes tipos de acciones como "INCREMENT", "DECREMENT" y "CHANGE_APP_NAME". Cada una de ellas devuelve una nueva copia del estado, manipulando si hace falta cualquiera de sus propiedades.
Por supuesto, como todo reducer, si no se entrega una acción, o las que se solicitan no se han implementado, se devolverá simplemente el estado actual (esta es la rama default del switch).
Creando las acciones (Action creators)
Como debes saber, cada vez que se desea cambiar el estado de nuestra aplicación, se debe disparar una acción mediante el store. La acción es un objeto plano que debe especificar el tipo de acción y, opcionalmente, datos necesarios para realizarla. Lo más adecuado es que mantengas centralizadas las creaciones de las acciones, por medio de funciones que se encargan de facilitar la tarea de definición de cada acción. Esas funciones se conocen como "action creators".
El código de nuestros action creators lo vamos a colocar en un módulo llamado redux/actions/actions.js
.
// acción para incrementar el contador
export const incrementarContador = () => {
return {
type: 'INCREMENT'
}
}
// acción para decrementar un contador
export const decrementarContador = () => {
return {
type: 'DECREMENT'
}
}
// acción para cambiar el nombre de la aplicación
export const cambiarAppName = (name) => {
return {
type: 'CHANGE_APP_NAME',
name
}
}
Después de todo este setup deberíamos tener una estructura de carpetas como la de esta imagen:
Conectar componentes de LitElement con Redux
Ahora viene la parte más divertida, en la que podremos crear componentes que se van a encargar de conectarse al store, de modo que podamos recibir cualquier cambio que se produzca en el estado de la aplicación, así como enviar acciones cuando queramos cambiar cualquier propiedad del estado.
Para esta parte es donde vamos a usar el mencionado mixin "connect", que nos ofrece la librería "pwa-helpers" que hemos instalado ya como dependencia, en un apartado anterior de este artículo.
Antes de esto, tenemos que aclarar que en una aplicación podemos tener dos tipos de componentes:
- Conectados al store: estos componentes están directamente conectados al contenedor del estado, de modo que, si el estado cambia, serán notificados con los nuevos valores que tenga el store.
- Desconectados del store: estos componentes no saben cuándo el store ha cambiado un dato, no reciben notificación ninguna cuando esto ocurre.
Mixin connect
El mixin connect nos sirve para desarrollar componentes que queremos que estén conectados al store. Para poder implementarlo básicamente tenemos que importarlo y luego usarlo al declarar la clase del componente.
import { connect } from 'pwa-helpers';
class ReduxLitelementApp extends connect(store)(LitElement) {
// ...
}
Como puedes ver en el código anterior, para usarlo necesitas enviarle el store como parámetro. El resultado de la ejecución de connect(), enviando el store al que nos conectamos, es el mixin que tendremos que aplicar.
Este mixin tiene la funcionalidad de avisar al componente cuando se realizan cambios en el store. Para ello, tenemos que escribir un método llamado "stateChanged" en el componente, que recibe el estado nuevo cada vez que éste cambia.
stateChanged(state) {
console.log('statechanged', state);
this.appName = state.appName;
this.counter = state.counter
}
Cuando el estado cambie, el método stateChanged() se ejecutará. La tarea que se suele hacer en el método es simplemente almacenar aquellos datos que el componente necesite manejar, en propiedades del propio componente.
Componente raíz, conectado al store
Comenzamos viendo cómo se conecta un componente al store. Un buen ejemplo para ello es el componente raíz de la aplicación, que generalmente necesitará conocer varios datos del store.
Para ello necesitamos importar una serie de cosas y luego usarlas al implementar Redux. Lo básico ya lo conoces, así que vamos directamente a ver el código fuente:
import { LitElement, html } from 'lit-element';
import { connect } from 'pwa-helpers';
import { store } from './redux/store';
import './counter-user-interface';
import './show-counter';
class ReduxLitelementApp extends connect(store)(LitElement) {
static get properties() {
return {
appName: { type: String },
counter: { type: Number },
};
}
render() {
return html`
<h1>${this.appName}</h1>
<show-counter counter="${this.counter}"></show-counter>
<counter-user-interface></counter-user-interface>
`;
}
stateChanged(state) {
console.log('statechanged', state);
this.appName = state.appName;
this.counter = state.counter
}
}
customElements.define('redux-litelement-app', ReduxLitelementApp);
La gracia aquí es que el componente recibirá el estado, de manera transparente para nosotros, cada vez que el store se manipule. Los datos del estado los podemos usar en el template, de modo que si cambian, cambiará la vista de nuestro componente. Por supuesto, los datos también los podemos pasar a componentes hijos, si estos los necesitan, como es el caso del binding de show-counter.
Usando este mismo esquema puedes crear componentes conectados al store, en cualquier punto del árbol de componentes de tu aplicación. Si usa el connect mixin, entonces podrá recibir los cambios del store.
Disparar acciones
Como sabes, necesitamos disparar acciones cada vez que queremos que un dato del store cambie. No necesitamos específicamente que el componente esté conectado al store para disparar acciones (no necesita implementar connect mixin), lo que sí necesitamos es importar el propio store, que se usa para despachar las acciones.
Nos apoyamos en los creadores de acciones que tenemos en el módulo de actions.js, que nos aportan una serie de funciones ayudantes, para que las acciones se creen con la sintaxis exacta que se necesitan.
Este es el código de un componente capaz de disparar acciones:
import { LitElement, html } from 'lit-element';
import { store } from './redux/store';
import { incrementarContador, decrementarContador } from './redux/actions/actions'
export class CounterUserInterface extends LitElement {
render() {
return html`
<hr>
<button @click="${this.incrementar}">Incrementar</button>
<button @click="${this.decrementar}">Decrementar</button>
`;
}
incrementar() {
store.dispatch(incrementarContador());
}
decrementar() {
store.dispatch(decrementarContador());
}
}
customElements.define('counter-user-interface', CounterUserInterface);
Como puedes ver, necesitamos importar el store, así como los creadores de acciones del módulo de actions.js.
Luego usamos los creadores de acciones para despacharlas, igual como se hace comúnmente en las aplicaciones Redux, con store.dispatch(), enviando la acción correspondiente.
Solucionar el error process is not defined
Nos falta un pequeño detalle para acabar y es que la dependencia "redux" asume que existe una propiedad "process" como variable de entorno. Esto se detalla en esta issue: https://github.com/reactjs/redux/issues/2907
Si ejecutamos la aplicación en estos momentos probablemente recibamos el error: "Uncaught ReferenceError: process is not defined".
Se soluciona fácilmente con un pequeño código en el archivo index.html de nuestra aplicación.
<script>
window.process = { env: { NODE_ENV: 'production' } };
</script>
Conclusión Redux & LitElement
Hemos aprendido lo básico para comenzar a entender Redux en el marco de una aplicación con LitElement. No necesitas mucho más para poder crear tu primera aplicación de ejemplo funcionando con Redux como contenedor del estado.
En este artículo he mostrado segmentos de código de una aplicación sencilla de ejemplo de Redux con LitElement. El ejemplo completo para posibles consultas y para jugar con el código está en este repositorio de GitHub: https://github.com/EscuelaIt/simple-redux-litelement. También puedes acceder al demo de la aplicación funcionando.
Hasta aquí las cosas han sido más o menos sencillas. La cosa se complica un poco más cuando necesitas conectarte a servicios web o realizar otras operaciones asíncronas, o cuando quieres implementar mecanismos de lazy load de action creators y reducers. Todo esto no lo hemos cubierto todavía, pero puedes aprender bastante más en el Curso de Aplicaciones Progresivas con Web Components y LitElement y en el propio PWA Starter Kit del ya que hemos hablado.
Miguel Angel Alvarez
Fundador de DesarrolloWeb.com y la plataforma de formación online EscuelaIT. Com...