Con el diseño implementado como una estructura JSX con estilos que genera un HTML/CSS solo queda ponerme manos a la obra y proveerla de interacción. Ahora tengo que ir pasando información vía props a mis componentes y estos ya sabrán cómo deben mostrarse. Obviamente en la anterior fase quedaron muchas cosas en el tintero y otras saldrán durante el desarrollo, pero lo importante es que para la vista que he desarrollado no tendré que escribir nuevos componentes ni modificar la jerarquía. Esto acelera muchísimo el desarrollo y permite centrarse en lo que es importante en cada momento. Que ahora mismo es que la página no solo muestre cosas, sino que haga cosas. Voy a partir esta entrada en dos partes puesto que si no se puede hacer infinito de leer (y más aún de escribir).
Preámbulo
En esta entrada voy a describir el proceso realizado para proveer a los componentes creados en el anterior capítulo de interacción y datos. Me voy a ocupar tan solo de la parte teórica, que es relativamente densa, y en la segunda parte de esta entrada mostraŕe como estoy aplicando esa teoría en mi propia aplicación.
Stack
Para proveer a los componentes de todas las capacidades descritas me voy a apoyar casi exclusivamente recompose.
Desarrollo
Como he comentado, la idea es proveer a los componentes funcionales o tontos de cierta lógica. Si no pensaramos demasiado las cosas, nuestro instinto nos mandaría transformar nuestros componentes funcionales en componentes statefull. O lo que es lo mismo, incrustar la lógica de negocio de nuestra aplicación en nuestros componentes. Algo así:
En el ejemplo, el mismo componente sin lógica (como lo dejamos en nuestra maqueta en la anterior entrada) y con lógica. Si tomamos la segunda vía, la del componente con estado o statefull, conseguiremos entre otras cosas romper la separación entre vista y controlador puesto que la lógica de representación y la de negocio se encuentran en el mismo compartimento. Esto es preámbulo sí o sí de código caótico, spaguetti, poco mantenible, difícilmente debugeable y un largo etc de cosas que no queremos.
Así que lo que crearemos en lugar de ello serán contenedores: Componentes con lógica que no renderizan jsx, sino otros contenedores o componentes funcionales. En esta entrada menciono mucho componentes y contenedores. Para no liarnos: Lo que pinta cosas con partículas elementales de jsx (divs, h1, span, etc) es un componente, lo que lo envuelve es un contenedor. Transformemos lo anterior en uno de ellos:
De este modo como se muestra en pantalla la información será responsabilidad del componente. La lógica que determina el qué se muestra, será responsabilidad del contenedor. Imagina ahora que el contenedor requiere más lógica. Dado que ya tenemos la infraestructura, podríamos ampliar nuestra funcionalidad añadiendo al mismo más estado y manejadores. Por ejemplo, un contador:
Y el componente con las nuevas props:
Genial, ahora a parte de un toggle para activar/desactivar nuestro componente, tenemos un contador con manejadores para incrementar y reiniciar. Pero esto también plantea problemas ¿Y si tengo otro componente que también necesita un contador? Podría:
- Duplicar la lógica en otro contenedor, provocando que tenga que mantener el mismo código dos veces.
- Usar el mismo contenedor, aunque el segundo no necesite la lógica de activación y desactivación.
En este ejemplo tan tonto ninguna de las dos opciones son problemáticas, pero escalalo a 50 contenedores con 20 propiedades cada uno y empezaras a ver que no es la manera óptima de proceder. Por suerte, podemos optar por una tercera vía:
Hemos separado nuestro contenedor en dos y cada uno maneja ahora su lógica. El superior maneja la de activo/inactivo y el inferior el contador. Hemos conseguido separar las responsabilidades, pero aún no hemos podido hacerlos reutilizables: Si te fijas en la línea 14, siempre que usemos el primero usaremos el segundo. Nos hemos quedado como estábamos. Pero ¿Y si hacemos lo siguiente?:
Hemos envuelto el primer contenedor en una función. De este modo, el contenedor/componente viene dado como dependencia inyectada como argumento de la función, y esta nos devuelve un OnOffContainer que renderiza lo que nosotros queramos. Ahora nos quedaría convertir nuestro CounterContainer en otra función análoga a esta y pasarle nuestro componente funcional (en el ejemplo, Comp) como dependencia. ¡Arreglado!
¡No tan rápido! Aun hay algo que falla ¿Y si lo hacemos al revés, que el OnOffContainer sea envuelto por el CounterContainer? Si vuelves hacia arriba dos imágenes, verás que CounterContainer espera las props que pasa OnOffContainer (active, y togleActive), pero no al contrario. Por lo que siguen sin ser reutilizables. Nuestros contenedores deben ser agnósticos en lo que respecta a propiedades que no necesiten: No les debe importar cuantas ni cuales sean, simplemente deben mandarlas hacía el siguiente componente en su método render. A esto lo llamamos prop forwarding. Por suerte, react y JavaScript convierten esta labor en un juego de niños:
Como ves, ahora los contenedores pasan toda su objeto props a sus hijos utilizando el object destructuring en las líneas 17 y 39, por lo que si reciben una prop tienes por seguro que su hijo la recibirá también. En la línea 46 puedes comprobar cómo generamos el componente mejorado: Nuestro componente funcional, envuelto por un contenedor que provee de un contador, y a su vez de otro que provee un on/off. La lógica representacional está aislada y la lógica de negocio compartimentada y reutilizable ¡Misión cumplida!
Patrón HOC y recompose
Para entender esta sección debes estar familiarizado con las arrow functions de javascript: https://developer.mozilla.org/es/docs/Web/JavaScript/Referencia/Funciones/Arrow_functions
Una función de orden superior es una función que o bien recibe, o bien devuelve una función. Es un concepto propio de la programación funcional y permite desarrollar patrones muy interesantes. Imagina, por ejemplo, que tienes una aplicación de finanzas en la tienes que aplicar una tasa constante a unas cantidades. Normalmente lo harías así:
Como tienes que realizar dicha operación sobre muchas cantidades distintas, la abstraes a una función withTax, pasando la cantidad como parámetro:
Ahora imagínate que tenemos varios tipos de tasas. Podríamos escribir una función para cada una de ellas, pero estaríamos repitiendo la operación y cambiando la cantidad. En vez de eso, podemos escribir una función de orden superior withTax que nos devuelva esas funciones parecidas, pero distintas. La función recibe la tasa como argumento y devuelve otra función que multiplica dicha tasa por la cantidad que reciba.
De modo que tienes una función que genera funciones y te ahorras escribir esa lógica una y otra vez. Ahí tienes tu función de orden superior.
Composición
Ahora imagina que tienes otra función withBonus que suma una cantidad fija al parámetro que recibimos, y otra función toEurString que transforma un número en una cadena con la forma «Amount X€». Vamos a escribir esas funciones y a aplicarlas sobre nuestra cantidad:
Todo bien. Pero ¿Y si tenemos que realizar esa misma operación sobre muchas cantidades? Podemos escribir una función que realice las tres operaciones una detrás de otra sobre un valor fijo, utilizando como argumento el resultado de aplicar la anterior. A esto se le llama componer funciones, y nos permite construir lógica compleja (la función final) a partir de lógica sencilla o unitaria (cada una de nuestras funciones). Podríamos hacer esto escribiendo la función getResult en la línea 5:
Not bad, pero resulta muy verboso y rígido. Por suerte existe una función de orden superior que hace lo que estamos haciendo nosotros en esa línea 5: compose. Muchas librerías nos proveen de esta función: Redux, Lodash, Ramda, Apollo o Recompose. Con compose nuestra función se vería así:
Nos abstraemos del argumento, dado que el hecho de que es solo uno y se llama secuencialmente a la siguiente función con el resultado de la anterior va implícito. Tendríamos la misma función que antes, pero sin tener que escribir de mas: Simplemente qué funciones queremos encadenar y en qué orden.
Componente de orden superior
Volvamos a nuestros contenedores y componentes. En esta imagen:
Utilizamos una función para configurar nuestros componentes. Y en la línea 45 utilizamos el resultado de withCounterContainer como argumento para withOnOffContainer. Vamos, que realizamos una composición de funciones que devuelven componentes (o contenedores) react. Esas funciones son lo que llamamos componentes de orden superior (en analogía de las funciones de orden superior), Higher Order Components o como te los vas a encontrar por ahí, HOCs. Y, al igual que las funciones, los podemos componer. De modo que lo que escribimos en la línea 46 podemos expresarlo como:
Siendo enhancer otro hoc, compuesto a partir de los otros dos. Esto nos permite reutilizar esa funcionalidad compuesta para añadirla a cualquier componente presentacional, o para componer y crear HOCs aún más complejos. Y esto sería el patrón HOC: Aportar funcionalidad compleja a partir de las funcionalidades simples que nos aportan contenedores que se envuelven entre sí y, finalmente, a un componente presentacional. Entre las ventajas de este patrón se encuentra:
- La reutilización de código es total, puesto que las funciones complejas se nutren de las más sencillas. Nunca escribes dos veces una funcionalidad sencilla, sino que la reutilizas.
- Desarrollar nuevos HOCs es muy sencillo a partir de las unidades más pequeñas. Este patrón te obliga a pensar de lo pequeño a lo grande y escribir mejor código.
- Al estar tan compartimentado, es muy fácil testear la funcionalidad que estas implementando.
- Dado que el inspector te dice en qué componente (en este caso, contenedor) se producen los fallos, debugear tu código es más sencillo si la lógica está compartimentada.
- Las abstracciones que realices son mas reutilizables entre proyectos con este patrón. Por ejemplo, yo tengo un hoc universal que provee paginación sobre cualquier array.
Entre las desventajas: Un mal naming te puede llevar a colisiones de nombres en tus props y la gran cantidad de ruido que mete en el inspector (genera unas cascadas de componentes muy serias). Un precio bastante bajo a cambio del potencial de este patrón. Los HOCs fueron introducidos por Sebastian Markbåge (@sebmarkbage) en 2016 y funcionan como los decoradores o mixins de React, sin las desventajas de unos y otros. Casi todas las librerías de primera línea del mundillo react exponen uno a varios HOCs en su API: Redux, Apollo, React-Router, etc… De hecho, si ya estas en el mundillo React lo más probable es que ya hayas usado algunos de ellos, siendo connect de redux el más famoso.
Recompose
Recompose es una librería que expone HOCs y factorías de HOCs para generar contenedores sin escribir clases de React. Se basa en el hecho de que a nivel de funcionalidad atómica (indivisible, para que nos entendamos), los contenedores siempre tienen la misma forma: Por ejemplo, un contenedor con estado va a exponer por medio de props su propio estado y manejadores para ese estado ¿Por qué deberíamos escribir clase, constructor, prop forwarding, prop injection y demás si podemos utilizar una función de orden superior? Ahí es donde va recompose. Este HOC de aquí:
Podemos escribirlo así:
Mucho más corto, limpio y conciso. Exactamente la misma funcionalidad. Una vez conoces la API de recompose, desarrollar HOCs es extremadamente rápido y sencillo. En Redradix lo hacemos de forma extensiva y nos funciona maravillosamente bien. El año pasado mi compañero Carlos de la Orden (@charliedjis) dio una charla magnífica a este respecto:
Recompose es obra de, entre otros, Andrew Clark (@acdlite), que un año despues de sacar la librería entro al core team de react en facebook. Vamos, que no es una idea feliz, sino que está muy ligado a cómo hacemos las cosas en React. Puedes consultar la API y ver la de cosas que se pueden hacer en su repositorio en GitHub.
Por otro lado, esta semana se han anunciado los nuevos hooks de React, que van a revolucionar la forma en que enhanceamos los componentes. A raíz de esto, Andrew Clark ya ha dicho que hasta aquí ha llegado el desarrollo de recompose. Por tanto, estamos en el momento de máximo desarrollo de la librería y probablemente del patrón, y a las puertas de lo que estaremos haciendo todos en un par de años. Creo que aún hay mucho jugo que sacarle, pero quizás prefieras estudiar el nuevo patrón que viene.
Hacía el HOC semántico
Desde hace un tiempo, estoy tratando en mis proyectos de ofuscar la lógica interna de mis HOCs exportándolos con nombres significativos y utilizando estos nombres para describir qué lógica compleja esconde un enhancer. Ok, no has entendido nada de esa frase, yo casi qué tampoco. El tema es que intento no escribir esto:
Y en su lugar escribir esto otro:
A este patrón lo llamo HOC semántico: Uno que intenta expresar su funcionalidad a partir de la descripción de sus partes. Es de developer flipado y no siempre funciona bien, pero me ayuda a:
- Ver de un vistazo qué está recibiendo un componente funcional cuando le aplico ese enhancer.
- Analizar las responsabilidades de cada HOC: Si no soy capaz de describir con pocas palabras lo que hace, es que debo partirlo en dos o más unidades más pequeñas.
- Facilitar que otro compañero mantenga el código expresando la intencionalidad de cada parte.
Es obvio que te expones a colisiones de nombres y a dejar dentro de esas cajas negras lógica defectuosa, pero ya he desarrollado un par de proyectos siguiendo este patrón y, en general, funciona bastante bien.
Punto y coma
Como dije en la anterior entrada, estoy bastante más activo en este proyecto gracias al reto de los 100 días de código. También dije que debido a ello, me gustaría publicar en el blog cada dos semanas. Hoy se cumplen esas dos semanas, por lo que he decidido partir esta entrada en dos, dado que me queda bastante por contar de este capítulo de añadir interacción a la maqueta y el tiempo me dá para lo que me da. Así que lo dejamos aquí por hoy. Espero que te haya parecido interesante este patrón y que pruebes a meter un poco de recompose en tus proyectos. En la segunda mitad de esta entrada os contaré como he definido el modelo de datos de mis posts, como he hecho la página reactiva al scroll, y como he hecho que los post sean navegables, todo ello con HOCs.
Hasta la próxima entrada!