Redruco: Truco para aprender Redux (Parte 1)
Vamos a hacer un tutorial paso a paso para construir una aplicación usando redux. Asumo que ya hicieron al menos una aplicación simple con ReactJS y conocen los conceptos básicos de: component, ciclo de vida del componente, props, state, y el patrón natural que surge en React de “pull-up” state (es decir subir el estado a los componentes más altos en la composición para así poder propagarlo y compartirlo entre diferentes componentes).
Motivación
Lo primero a entender son los problemas que trae manejar el estado de la aplicación en los componentes de React (state)
- Acoplamiento estado-vista: estamos acoplando cuestiones visuales (ya sea “cómo se ve” o bien como se interactúa a nivel de UI -clickear, mouse over, etc) con el estado.
- En particular lo que nos va a complicar son las mutaciones de estado. Pero también podría ser algún tipo de manipulación para generar estado calculado, o bien “masajear/reestructurar” el estado para mostrarlo (ej, agrupar -groupBy-, o combinar colecciones de obejtos, etc)
- Propagación de estado: en react “puro y duro” la única forma de compartir estado entre los componentes es a través de las properties. Si tenemos un árbol de componentes complejo( lo cual es bastante común, porque la filosofía de react es poder partir una pantalla en componentes bien chiquitos reutilizables) y las “hojas” necesitan acceder a cierto objeto/dato que está como state de la raíz, entonces eso significa que tendremos que manualmente arrastrar/propagar ese estado a través de todos los componentes intermedios hasta llegar a las hojas. Incluso si esos componentes intermedios no hacen nada con ese dato. Actúan simplemente de “pasamanos”.
- Esto último tiene 2 consecuencias, ambas “graves” para toda aplicación mediana / grande: 1) impacto en refactors: si cambia ese estado, tenemos que modificar todos los componentes (ej, si se agregar un dato más). Propenso a error. 2) impacto en performance: react re-renderiza un componente si sus props o bien su state se modificaron. Si mi aplicación propaga estado por todos lados, entonces cuando se modifique ese estado, entonces para poder actualizar esos componentes “hojas” que son los que realmente deberían ser afectados para mostrar una actualización, estaremos forzando a re-renderizar tooooodos los componentes intermedios que simplemente hacían de pasamanos. Lo cual no tiene mucho sentido ya que visualmente se van a ver igual. Esto no es un problema instantáneamente en una app React, porque éste utiliza un dom virtual para sólo actualizar el DOM que realmente es distinto. Sin embargo nos estamos comiendo muchísimo overhead.
En conclusión nada bueno sale del modelo “pull-up” y manejo de estado de React “puro”.
Entonces aparecen los State Containers / Managers para atacar específicamente la complejidad de manejar estado en una aplicación independientemente de la vista.
Redux es uno de ellos, y el que vamos a ver.
Qué vamos a construir
Entonces para hacerlo un poco más divertido que la famosa “TodoList” vamos a implementar un juego de truco. Con bastantes limitaciones, ya que es un juego bastante complejo.
Ya veremos cuánto podemos agregarle sin que se haga demasiado extenso.
Experiencia de Usuario
Hay varias formas de encarar el desarrollo. Yo voy a arrancar pensando cómo será la experiencia de usuario. Si bien el “estado” más básico de la aplicación se podría pensar como independiente de detalles “visuales”, ayuda a reflexionar sobre el mismo.
Entonces nos imaginamos algo así:
Aca vemos varias partes de la vista
- Cartas en mano: cada participante tendrá sus (3) slots para cartas en la mano. Las del oponente no se ven. Las nuestras serán clickeables en ciertos momentos (en nuestro turno al tener que elegir cual jugar).
- Visualizamos de quién es el turno: de alguna forma resaltará quién es el jugador actual que debe realizar una acción
- Cartas en juego: en el medio de la “mesa” de alguna forma mostraremos las cartas jugadas en cada mano.
- Puntaje: Nada.. lleva la cuenta del juego. Vamos a imitar la vieja forma de contar con palitos, lo cual va a introducir algunas cositas interesantes de resolver de manipulación/transformación de datos.
Por ahora nos vamos a detener ahí. Sin contemplar “envido”, o el canto del truco, retruco, etc. Es decir, vamos a jugar una mano simple utilizando todas las cartas. De a poco introduciremos más funcionalidad.
Bootstrap
Arranquemos creando una app React comunacha.
# si no tenemos CRA
yarn global add create-react-appcreate-react-app 2018-redux-truco
cd 2018-redux-truco
Storybook
Si bien no tiene nada que ver con Redux, vamos a utilizar Storybook, una herramienta que nos va a permitir partir el trabajo de crear componentes react (vista), y poder trabajar tranquilos en un componente por vez, en los detalles visuales y de interacción (disparar eventos llamando a funciones/callbacks props).
Es una muy buena práctica (casi como TDD :)) para evitar acoplar los componentes con el estado. Y nos permite trabajar sobre un componentes sin tener que usar la aplicación, ya que muchas ciertos componentes sin dificiles de trabajar, ya sea porque desparecen rápidamente, o porque llegar a ellos requiere varias interacciones molestas, etc.
(nota: yo lo instalé y ejecuté localmente, en parte porque no me anduvo globalmente -parece que el script usa ES con imports-, pero también porque trato de evitar instalar dependencias “globales”, ya que pueden colisionar diferentes proyectos)
# instalar
yarn add -D @storybook/cli# setupear
node ./node_modules/@storybook/cli/bin/index.js# ejecutar (de acá en más)
yarn storybook
Componentes de vista
Vamos a ir entonces componente por componente, construyendo la vista independientemente de la lógica de juego y de estado.
En general es más fácil ir atacando de lo concreto y chiquito a lo más grande y abstracto o layouts.
Card
Creamos una story para las cartas y el componente Carta
mkdir -p src/components/
touch src/components/Carta.jsx
touch src/components/Carta.csstouch src/stories/Carta.jsx
El componente tendrá por ahora algo así
import React from 'react'import './Carta.css'export default function Carta({ carta }) {
return (
<div className="carta">
{carta.numero}
{carta.palo}
</div>
)
}
CSS
.carta {
width: 100px;
height: 200px;
background-color: blue;
}
Y el storybook
import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'import Carta from '../components/Carta'storiesOf('Carta', module)
.add('3 de basto', () =>
<Carta carta={{ numero: 3, palo: 'basto' }} />
)
Modificamos src/stories/index.js
para incluir este nuevo story
// Agregamos al final del archivo
require('./Carta.jsx')
Y a partir de ahí podemos iterar para hacerla más linda :)
Sin ser un as del CSS, después de jugar quedaron así
Pueden ver el código fuente de Card.jsx
y Card.css
para entender cómo está hecho.
Puntaje
De igual manera podemos proceder a concentrarnos en el componente que va a mostrar el puntaje. Para eso definimos que el puntaje será un objeto así
{
nosotros: 13,
ellos: 6
}
Para eso de nuevo creamos un componente
touch src/components/Puntaje.jsx
touch src/components/Puntaje.css
touch src/stories/Puntaje.jsx
Y aquí una primera versión un poco precaria. Donde en lugar de cajitas vamos a mostrar un número de 1 a 5. Luego veremos como renderizar una cajita. En particular el problema es trazar una linea en diagonal :P Pero ya queda preparado para luego cambiar sólo el componente Cajita
El Componente:
Un par de aclaraciones sobre esto:
- Partimos el componente en 3 partes. Un contenedor que tiene como 2 “columnas” el puntaje nuestro y el del oponente. Como son muy parecidas esas columnas reutilizamos mediante el componente
ColumnaPuntaje
y a su vez cada cajita es un componenteCajita
. Que por ahora muestra un número. - Probablemente la forma en que logré estilearlo como una tabla (o la decisión misma de no haber usado tablas) sean criticables, y seguro hay una mejor manera de hacerlo. Como dije, no soy experto en CSS :P
- Estamos haciendo un “masajeo” de los datos iniciales
{ nosotros: 3, ellos: 16 }
a una estructura de datos distinta, de modo de que sea más fácil armar las cajitas de palitos luego. Esa otra estructura para este ejemplo sería palitosNosotros[ 3, 0, 0 ]
y para palitosEllos[ 5, 5, 1 ]
- Ese masajeo no conviene hacerlo en el componente como está hecho, porque.. es lógica que tranquilamente podría desacoplarse y trabajarse con tests. Es una de esas cosas que inicialmente pensamos como “tontas”, pero propensas a bugs. Por ahora lo dejamos acá, pero porque todavía no vimos un concepto que vamos a necesitar (selectores). Igualmente vale sacarlo a una función :)
- Para implementar dicho masajeo agregamos una librería para programación “funcional” llamada
ramda
. Queda como ejercicio “para el hogar” analizar y entender qué hace esa lógica de pipes, maps, repeat, etc :)
yarn add ramda
El CSS
Y el storybook
import React from 'react'
import { storiesOf } from '@storybook/react'
import { action } from '@storybook/addon-actions'import Puntaje from '../components/Puntaje'storiesOf('Puntaje', module)
.add('nos 3, ellos 10', () =>
<Puntaje puntaje={{ nosotros: 3, ellos: 11 }} />
)
.add('nos 0, ellos 1', () =>
<Puntaje puntaje={{ nosotros: 0, ellos: 1 }} />
)
.add('nos 0, ellos 0', () =>
<Puntaje puntaje={{ nosotros: 0, ellos: 0 }} />
)
Mano
Una mano será un componente simple por ahora para presentar las 3 cartas del usuario. pero también nos permite reflexionar sobre el estado. Cada carta puede haber sido jugada o no.
Y ya contemplamos la idea de que esas cartas pueden haber sido “jugadas”
Mano.jsx
CargaJugada.jsx
Mano.css
.mano {
display: flex;
justify-content: space-evenly;
}
Y modificamos Carta.css para estilear la jugada
/* cartajugada */
.carta.jugada {
opacity: 0.5;
background: brown;
}.carta.jugada > div {
background: rgb(50, 15, 15);
border: none;
}
Storybook
Mesa
La mesa será el componente donde se van a mostrar las cartas ya jugadas. A nivel de experiencia de usuario es importante que fácilmente transmita el estado del juego, es decir cuántas manos de las 3 posibles ya se jugaron y quien gano cada mano.
Vamos a modelar los datos de la siguiente forma
O sea, una lista cuyos elementos representan: 1) primera mano, 2) segunda, ..3ra.. se entiende :P O sea.. de 0 a 3 elementos.
Cada mano tiene nosotros
y ellos
con la carta jugada (o undefined si todavía no se jugó. Y además un resultado
que puede ser:
ganador
perdedor
empate
Y en base a eso se muestran los tildes debajo.
Queda algo así (tal vez cambien los estilos al juntar todo :P)
El código de los componentes en Mesa.jsx
Y los estilos
Y el storybook
Nota: si sos muy obsesivo te estarán molestando esos strings “repetidos” que tenemos ahí:
- palos:
copas
,oros
,bastos
yespadas
- resultado:
ganador
,perdedor
yempate
Ambas cosas pertenecen claramente al modelo de dominio y no a la vista. Así que podríamos extraerlas a constantes:
mkdir src/model
touch src/model/constants.js
Juego
Ahora sí tenemos que unir todo con un componente que represente todo el panel de juego. Acá ya vamos a estar en problemas porque necesitamos decidir cómo se va a propagar el estado entre los componentes.
Como la forma cambia radicalmente entre usar Redux o React plano, por ahora vamos a hardcodear esos estados.
touch src/components/Juego.jsx
touch src/components/Juego.css
touch src/stories/Juego.jsx
Acá una primera versión del componente, con el estado hardcodeado
Y los estilos:
Y con un storybook muy simple:
Se ve así
Ahora, tuvimos que hacer unos cambios para ocultar las cartas del oponente. Quedó una solución que se podría mejorar :P
Un nuevo componente
Además nos cansamos de escribir los classNames con string interpolations para esos casos donde un estilo se debe aplicar condicionalmente, así que agregamos la librería classnames. Recomendamos leer la documentación. En este caso va a aplicar la clase CSS jugada
sólo si se cumple que la prop jugada
es true.
yarn add classnames
Y finalmente modificamos Mano
para contemplar si es de oponente
export default function Mano({ cartas, seleccionable, oponente }) {
const crearCarta = oponente ?
carta => <CartaOponente jugada={carta.jugada}/>
: carta => carta.jugada ?
<CartaJugada /> : <Carta carta={carta} seleccionable /> return (
<div className="mano">
{cartas.map(crearCarta)}
</div>
)
}
Y agregamos estilos en Carta.css
/* carta oculta (oponente */
.carta.oponente {
background: brown;
}.carta.oponente > div {
background: rgb(50, 15, 15);
border: none;
}
Listo, tenemos ya una base de componentes como para empezar a integrarlos y trabajar en el estado con redux.
Pero eso en la 2da parte :)
Espero que les haya resultado divertido. Si bien todavía no vimos ni una linea de Redux, eso es justamente una de sus “ventajas” que nos permite pensar el frontend “partiendo” las problemáticas de vista y de estado como concerns separados, que podemos atacar en diferentes momentos. Eso nos da una “forma de trabajo”, ordenada e incremental, que es lo que estamos tratando de plasmar acá en estos posts.
Disclaimer: Si bien vimos la idea de crear componentes (vista), estilearlos (?) y probarlos (manualmente) con storybook, nos faltó otra pata importante del desarrollo de la vista que son los tests. A cada componente le podríamos haber creado un test de vista, ya sea con jest, o bien con enzyme. No lo hicimos para no extender demasiado el post, pero es algo interesante para tener un frontend saludable :)