Redruco: Truco para aprender Redux (Parte 2)

Continuamos implementando la versión reducida del truco en React + Redux. La parte 1 está en este link

Ahora sí, una vez que avanzamos bastante con los componentes de vista vamos a pensar en el estado y sus mutaciones, es decir vamos a lo que vinimos, aprender Redux.

Nota:
No hay un “orden” de trabajo, yo elegí arrancar por la vista, pero tranquilamente podríamos haber arrancado implementado el estado y sus mutaciones, y tener andando un truco sin vista. Y luego crear los componentes. Finalmente la idea es que la vista con React de esta forma queda con una interfaz “bien definida” y desacoplada del estado. Y Redux nos va a dar una forma simple de conectar esos dos mundos.
Incluso, si se coordina bien y se mantiene una buena comunicación, se podrían trabajar en ambos frentes al mismo tiempo entre varias personas

Breve intro a Redux

Hay muchísima documentación en internet, e incluso la página oficial de documentación de Redux hace un muy buen trabajo en explicar los conceptos, motivación y las “máximas” de redux.

Voy a tratar de resumir acá lo que me parece más importante.

Store/State

El store:

  • En redux modelamos el estado de nuestra aplicación en lo que llamamos el state, similar al state de un componente de React.
  • Ese state vive en un store de redux.
  • Tendremos un único store (y por ende state) para toda la aplicación (una máxima de redux)

El State

  • es inmutable
  • es una estructura de datos, un JSON

Bootstrap de Redux

Instalamos la librería redux en sí, y otra que nos permite conectar componentes de react con redux

yarn add redux react-redux

Como dijimos tendremos un único store y deberá ser accesible desde cualquier componente de React para poder “conectar” ese componente con el state.

react-redux nos trae un Provider que hace eso, utilizando la API de Contextos de React. Tenemos que configurarlo entonces

Modificamos index.js

Lo que hicimos fue envolver <App> en <Provider> que es un componente de react-redux. A ese componente le pasamos un store, que creamos con un nuevo archivo

touch src/storeCreator.js

Con contenido muy pavo por ahora que crea un store muy simple de redux. Pero es muy común luego tener que tunear un poco el store con elementos extra, así que ya lo dejamos en un archivo propio encapsulado

import { createStore } from 'redux'
export default function() {
return createStore(_ => _)
}

El parámetro (_ => _) es algo temporal que vamos a cambiar luego.

Y por último creamos una estructura de directorios para los reducers y actions

mkdir src/reducers
mkdir src/actions

Modelando el state/store

Es hora entonces de definir cómo va a ser nuestro state de juego. Si vienen de ver la parte de los componentes, medio que ya fuimos tomando decisiones sobre el modelo de negocio: qué es una mano, el puntaje, etc. Es hora de pensarlo más globalmente.

De nuevo acá hay varias formas de encararlo

  • basado en eventos/actions: es decir, primero pensamos qué eventos suceden durante un juego, ej: “inicia el juego”, “inicia una ronda”, “se juega la carta X”, “termina una ronda”, “termina el juego”
  • basado en la forma del state: un poco más difícil y propenso a cambios más tarde es pensar como sería el estado puro en forma de datos.

Vamos por la primera

Iniciar Juego

Primeramente va a arrancar un juego. En redux, toda acción o evento que desemboca en una mutación del estado se modela con una idea llamada action. Los actions en su forma más simple son simples objetitos o estructuras de datos que:

  • tienen un type que nos servirá para identificarlos luego al momento de manejarlos y hacer una mutación
  • pueden tener cualquier otro dato específico de cada evento/action

Ejemplo:

{
type: 'INICIAR_JUEGO',
cartas: {
nosotros: [
{ numero: 1, palo: Palo.BASTOS },
{ numero: 4, palo: Palo.COPAS },
{ numero: 3, palo: Palo.ESPADAS },
],
ellos: [
{ numero: 7, palo: Palo.BASTOS },
{ numero: 1, palo: Palo.COPAS },
{ numero: 6, palo: Palo.OROS },
]
},
turno: 'nosotros'
}

Es decir indica: que se arranca un juego; que quien debe jugar primero somos nosotros; y contiene las cartas que se barajaron para cada jugador.

Ahora, un action no sale de un repollo, alguien tiene que crear este objetito, entonces vamos a tener una función que sepa crear este objeto, en base a parámetros, que serán las partes que “cambian” de un evento a otro.

Esto es un nuevo archivo llamado src/actions/juego.js Y ya es nuestro primer action de redux :)

Nótese que no tiene dependencias a redux ni a ninguna librería. Es simplemente una función normal Javascript, que sigue la convención de retornar un objeto con type (type es generalmente un string, útil para debugging, por ejemplo)

En la jerga de redux esto que hicimos se llama “action creator”. El action, técnicamente es el objetito que se construye.

Una de las cosas más interesantes de redux es que cada “partecita” que escribimos sigue esta idea de ser algo bien atómico y desacoplado de “parafernalia”. Con lo cual es muy fácil hacer testeo unitario.

Acá un test para este action creator.

touch src/actions/juego.spec.js

Parece medio pavo testear este tipo de actions. Dejemos la discusión para otro momento. Por ahora nos sirve para entender que esto es fácilmente testeable, y hasta, que uno podría hacer TDD :). Lo que sí podemos decir es que, este tipo de actions es muy pavo, pero no siempre es así. Existen otros tipos donde la función no es tan pava, y hace cosas complejas que definitivamente queremos testear como por ejemplo hacer uno o varios pedidos al servidor en forma asincrónica. O bien un action que ejecuta varios otros actions. Todas estas son variantes “avanzadas”. Se explican en la doc de Redux

Reducer Inicial

Perfecto, hasta ahora tenemos el action que indica que se comienza un juego. Sin embargo así sólo no hace nada. Entonces lo que tenemos que entender es que en redux hay 3 elementos que colaboran para manejar el estado:

  • store: quien tiene al state actual. Su otra gran responsabilidad es recibir actions a “manejar” a través de una función store.dispatch(action). El sabrá manejar ese action y actualizar el estado con el nuevo. Recordemos que el state es inmutable, por lo que cada dispatch de un action genera un nuevo state
  • action: es lo que vimos ya, una estructura que representa un “evento” o bien una “orden”, desacoplado de la lógica que realmente modifica el state
  • reducer: lo que vamos a ver. Es la función que sabe manejar el action, realizando la mutación que haga falta, en la forma de devolver un nuevo estado.

Un reducer tiene básicamente esta firma

unReducer(estadoActual, action) : nuevoEstado

Vamos a hacer el nuestro así nos damos una idea

touch src/reducers/juego.js

Y dentro vamos a hacer la función. Como es nuestro primer reducer, vamos muy de a poco.

export const juego = (state, action) => {
return state
}

Ese es el reducer más simple. No hace nada. Retorna el mismo estado que recibió.

Algo que normalmente hacemos es inicializar el state. En caso en que el parámetro state sea undefined, iniciamos uno

const initialState = {}
export const juego = (state = initialState, action) => {
return state
}

Ahora queremos que haga algo ante el action de INICIAR_JUEGO. Este es el patrón más común de código en los reducers: usar un switch para ejecutar distintas lógicas en base al type (por eso los actions tienen un type :) )

import { INICIAR_JUEGO } from '../actions/juego'
const initialState = { }
export const juego = (state = initialState, action) => {
switch (action.type) {
case INICIAR_JUEGO: return state
default: return state
}
}

Sigue sin hacer nada, pero ya podemos poner código ahí para generar el estado nuevo que será el “inicial de un juego de truco”.

Entonces acá ya avanzamos un poco a pensar el state. Va una propuesta

{
puntaje: { nosotros: 0, ellos: 21 },
ronda: {
turno: 'nosotros/ellos',
cartas: { nosotros: [ ... ], ellos: [ ... ] },
manos: []
}
}

Qué modelamos acá:

  • en puntaje vamos guardando los puntos de cada jugador.
  • Los elementos relacionados a la ronda actual los modelamos bajo ronda, así, luego será fácil reemplazarlo por uno nuevo cuando se juega una nueva ronda.
  • Esa ronda tiene 3 datos importante: turno (a quién le toca), cartas (estructura que contiene las 3 cartas barajadas para cada jugador), y manos que es una lista de 0 a 3 elementos, que representa la mesa, las manos jugadas digamos. Al iniciar, lógicamente está vacía.

Entonces, el reducer deberá crear este estado, con la salvedad de que:

  • turno: lo recibe del action mismo (action.turno). Así se pueden crear juegos donde arrancamos nosotros, o ellos.
  • cartas: el reducer no baraja, es bien pavo. En el action vienen las cartas ya barajadas (action.cartas, como ya vimos en el action)

Quedaría así entonces

Simple. Con los parámetros del action construye un nuevo estado y lo retorna.

Al igual que con los actions, los reducers son meras funciones JS. Así que podemos hacerle un test muy fácil, en términos de estadoActual => reducer => estadoEsperado

Parece un poco grande porque repetimos la estructura de datos para el assert. Pero también eso hace al test más simple de entender (que bien !), pero también más difícil de mantener (que mal !).

Y ahora ?

De nuevo acá tenemos diversos caminos a tomar, dependiendo de la forma de trabajo de cada uno. Podemos

  • seguir modelando los demás eventos del juego, con actions y action creators, y su correspondiente reducer. Y tener todo testeadito, pero aún sin poder ver nada en la ui.
  • conectar ya este action y algunos elementos de la UI al estado, de modo de poder ir viendo como reaccionan.

Vamos a ir por lo segundo, lo cual es bueno para personas más visuales :)

Conectando React => Redux

Vamos a conectar 2 cosas para poder ejecutar y ver cambios en la UI

  • El action iniciarJuego: a un botón para poder ejecutarlo.
  • El estado del puntaje actual: al componente de puntaje.

Es decir que la interfaz entre react y redux son esos 2 elementos: conectar actions y/o estado

Para eso react-redux provee un HOC. Es decir un “Higher-Order Component”, que es una especie de “decorator” o envoltura que podemos agregar a nuestros componentes, y que agrega cierta lógica.

En nuestro caso nos va a conectar al store, ya se estado ó bien actions, e inyectar dichos elementos como props a nuestro componente.

Connect de Action: iniciarJuego

Para eso creamos un nuevo tipo de componentes, los Container ‘s

mkdir src/containers
touch src/containers/Juego.jsx

Un detalle antes, tenemos que agregar un link / botón en el componente Juego.

export default function Juego({ onIniciarJuego }) {
...
return (
<div className="juego">
<div>
<a href="#" onClick={onIniciarJuego}>Iniciar Juego</a>
<Mano cartas={cartasEllos} oponente />
<Mesa manos={manos} />
<Mano cartas={cartasNosotros} />
</div>
<div>
<Puntaje puntaje={puntaje} />
</div>
</div>
)}

Nótese que agregamos un <a> que al clickearse invoca una función que recibe como props llamada onIniciarJuego()

(hay que tocar estilos para que se vea menos horroroso.. pero salteo eso)

Ahora sí, el container:

Como se ve estos “componentes” difieren muchísimo de los componentes tradicionales de React. No tienen tags, porque su función es simplemente “conectar” un action a una prop del componente que van a envolver.

Para eso usamos la función connect de redux que es una función que retorna otra función para envolver al componente. El resultado final es un componente que exportamos.

Connect recibe 2 parámetros:

  1. Para conectar “estado” del store al componentes (por ahora no lo usamos).
  2. Para conectar actions (creators) al componente.

En ambos casos genera valores que se van a inyectar en el componente como props, para que éste pueda usarlos como si fueran cualquier otra prop, independientemente de si viene de redux o no.

Esos parámetros son funciones: mapStateToProps, y mapDispatchToProps.

mapDispatchToProps es una función que recibe el dispatch (función que ya nombramos: sirve para enviar actions a redux, y por ende que se procesen por el reducer). Y lo que retorna es un objeto a modo de especificación con varios props: fn()

propA: () => ... dispatch de un actionA,
propB: () => ... otro dispatch de otro action
etc

propA, y propB son los nombres que tienen que coincidir con los nombres de props que el componente espera recibir.

Acá vemos un diagrama para entender toda la arquictura

Containers: permiten conectar el estado de la app (state / actions) a componentes “puros”. Fuente: https://giamir.com/unit-testing-a-react-redux-app

Ahora, tenemos que asegurarnos de usar este container/Juego.jsx en la aplicación en lugar de usar components/Juego.jsx

Para eso modificamos App.jsx

(importamos el container y lo agregamos al html)

Por último, para poder entender qué está pasando es recomendable usar una extensión de chrome: Redux DevTools

Esto requiere crear el store de redux con un cambiecito, y de paso vamos a arreglarlo, porque no le habíamos configurado el reducer todavía. Editamos storeCreator.js

Lo que hicimos fue cambiar el parámetro default que teníamos (_ => _) que ahora sabemos que es el reducer, para que use el nuestro. Y el 2do parámetro es para la herramienta de debugging en chrome.

Ahora levantamos la aplicación yarn start y hacemos botón derecho => inspect. Deberíamos ver una solapa “redux”.

Ahora sí podemos clickear el botón y vamos a ver que se generó un action y podemos ver cómo se modificó el state con la herramienta.

Probando primer action conectado y reducer

Connect de State: puntaje

Vamos a hacer algo similar para conectar “estado”. En este caso para mostrar el puntaje.

Un nuevo containers/Puntaje.jsx

La idea es la misma que con el componente anterior, lo conectamos a través del uso de connect() pero en este caso con el primer parámetro que mapea de state a un objeto con props que se van a inyectar en el componente.

El chiste es que entre Redux y React van a ponerse de acuerdo para que, cada vez que cambie state.puntaje se actualice este componente.

Tenemos que hacer un par de cambios para ver esto funcionando:

  • Juego.jsx debería ahora usar containers/Puntaje en lugar de componentes/Puntaje
  • También en Juego.jsx podemos volar el hardcodeo del puntaje const puntaje = { nosotros: 3, ellos: 11 } ya que usamos Puntaje container que no requiere la propiedad “puntaje” porque la “busca” del store de redux :)
  • Deberíamos cambiar el initState del reducer de modo de que de entrada puntaje sea algo así, y no rompa: { puntaje: { nosotros: 0, ellos: 0 } }
  • Por último para checkear que realmente está funcionando podemos tocar temporalmente el reducer, de modo de que en elcase INICIAR_JUEGO arranquemos con un puntaje arbitrario { puntaje: { nosotros: 10, ellos: 0 } }

Tipos de Componentes: Presentational vs Containers

Un poco de nomenclatura y resumen.

Lo que estuvimos haciendo es lo que en redux se llaman 2 tipos de componentes: Presentational y Containers.

Y es justo la división que hicimos nosotros. Primer atacamos componente por componente, sólo sus cuestiones estéticas, y de como presentar la información e interactuar. Esos son componentes puros que esperan recibir los datos y las funciones (callbacks) como properties. Los podemos pensar como funciones reutilizables, que no están atadas a un estado global. Esos son los Presentational.

Por el otro lado los Containers no atacan el problema de la vista, sino de como manejar el estado (state) y eventos (actions).

Esta separación parece un poco molesta, con un poco de boilerplate, pero es clave para tener una aplicación ordenada, reutilizable, y poder testear los aspectos en forma independiente (vista desacoplada del estado). Lo cual nos va a hacer la vida mucho más simple.

Relación entre store, container y presentational (fuente: https://giamir.com/unit-testing-a-react-redux-app)
Connect de actions y flujo de ejecución para mutaciones (fuente: https://giamir.com/unit-testing-a-react-redux-app)
Nota: acá seguimos la convención de separar en archivos e incluso carpetas distintas los containers. No es la única forma. A veces para evitar ese boilerplate uno puede escribir ambos componentes en el mismo archivo. Cuidando de exportarlos a ambos, el Presentational y el Container, de modo de poder usarlos independientemente, por ejemplo el Presentational en un test o storybook, y el Container en la app. O mismo para usar un mismo componente asociado a dos partes distintas del store, podríamos tener 1 Presentational, pero > 1 Containers.

Reflexiones sobre el State en redux

Acá algunas notas más avanzadas sobre las características del state. Lo separamos de las notas iniciales, para no confundir.

El State:

  • debe estar normalizado (sí ! recuerdan de base de datos :P volvió en forma de fichas, si venían desarrollando OOP, esto es polémico. Ver nota abajo).
  • Es conveniente tener una estructura plana, es decir evitar muchos niveles de anidamiento.

Normalización

“normalizado”, para quienes no estén familiarizados con el término se refiere a modelar las relaciones de asociación entre un objeto y otro mediante referencias en lugar de embeber directamente esa entidad.

Ejemplo, tenemos una Materia , y varios Alumno.

Modelo desnormalizado

Vemos que las materias están repetidas entre los alumnos por la relación Alumno.materias, pero también respecto de la lista de materias

Esa repetición hace las cosas muy complejas, porque si por ejemplo renombramos una materia, tendríamos que modificar en tooodos los lugares donde aparece. Es el típico error que luego aparece como partes de la interfaz que no se actualizan y es porque nuestro estado no estaba bien diseñado.

Acá el mismo ejemplo, pero normalizado

Tuvimos que introducir id’s únicos para cada entidad que queremos referenciar, y reemplazar los lugares donde aparecían las materias por su id.

En el mundo JS existen librerías que se suelen usar con redux para normalizar (/ desnormalizar) datos, como por ejemplo normalizr

Estructura Plana

Esto muchas veces se resuelve con la normalización, pero no siempre. Por ejemplo, si tenemos que modelar un filesystem o cualquier otra estructura arborea, podríamos estar tentandos a hacerlo “embebiendo” los documentos/objetos/JSON, y si no existen links o referencias cruzadas, no necesitaríamos la normalización estríctamente.

Ejemplo

El problema de este tipo de modelo es que cada vez que cambia un objeto de esos, también cambian todos sus contenedores, es decir cambia “toda la rama desde la raíz hasta el objeto”. Incluso si el cambio no es de “estructura” del arbol, es decir de las relaciones, como por ejemplo, si cambiamos el nombre del archivo nota.txt conceptualmente el arbol sigue siendo igual al anterior en su estructura, simplemente a ese nodito, el archivo, se le cambió el nombre.

Bueno, no funciona así. Como el state es inmutable tenemos que pensar que no cambiamos a ese archivo, sino que generamos uno nuevo, igual al que teníamos pero con el nombre actualizado.

Como es un objeto/documento nuevo, tenemos que también generar una carpeta nueva igual aadmin pero que tenga a ese archivo nuevo con el nuevo nombre. Y como estamos cambiando la carpeta admin, también tenemos que generar una nueva carpeta home, y por ende también un nuevo state que tenga ese nuevo home. etc..

Se entiende..cuanto más profundo es el state más objetos se ven afectados por los cambios.

Esto tiene 2 problemas. Por un lado la lógica para copiar todo, y actualizar cierto elemento profundo es muy molesta. Hay librerías como ImmutableJS que facilitan un poco esto, pero a expensas de contaminar todo el state con sus propias estructuras de datos (benzoato de potasio)

El otro problema es que tendrá un impacto en performance en la vista. Porque si cambia el nombre de una carpeta intermedia, también se renderizaran nuevamente los componentes asociados a nodos “hijos” de ese. Es decir hay un acomplamiento.

Si aplicamos la misma idea de normalización, nuestro modelo se “aplana” y en este caso (que no cambia al estrutura), sólo se modificaría el nodo del archivo y no sus contenedores. O en el caso inverso, se actualizaría el contendor (carpeta) y no cada uno de sus hijos. Actualizando sólo el componente “conectado” a él.

Lo que sigue

Todavía falta bastante, pero creanme que ya tenemos la base completa punta a punta de todo lo que tenemos que hacer. De acá en más, podemos volver a enforcarnos en los actions. Crear los restantes de jugar carta, etc.. y luego conectarlo a los componentes.

También queda pendiente entender los diversos tipos de actions creators más complejos. Generalmente requeridos para la comunicación con el servidor.

Lo vamos a seguir en la tercera parte