Conceptos de Redux
React JS es una librería excelente manejando la manipulación del DOM de una manera declarativa, facilitando lo que es manejar la vista, pero no era necesariamente bueno para manejar estado. A medida que React fue cogiendo mas tracción, empezaron a surgir problemas manejando el estado en aplicaciones a gran escala. Al salir React en el 2014, nació en la mitad de lo que se llamo The Flux Wars, que fue la carrera para decidir que librería iba a ganar supremacía en el ecosistema de React para el manejo de estado recomendado. Era claro para el equipo de React que el patrón que querían seguir era el Flux Pattern y en el 2015-2016 Redux gano las guerras de Flux, liderada por Dan Abramov, co-autor de Create React App que hoy en día trabaja directamente en React con Facebook. La influencia de Redux en React llevo a la creación de Hooks, el cambio de componentes de clase a componentes funcionales, la creación de el API de useContext y useReducer, y la creación de un ecosistema alrededor de Redux que lo vuelve en una herramienta muy versátil. Si ya conoces sobre los conceptos de Redux y quieres ver como aplicarlo lee la Guía Rápida de Redux
Nov 5 2020: Redux ha sido descargado 4,770,896 veces esta semana en npm.
El Problema: Patrones de React y Estado
React funciona utilizando lo que se conoce como el Virtual DOM para determinar los cambios que se deben hacer a la pagina y cambiar la vista que se presenta. Utiliza el paradigma declarativo porque no manipulamos el DOM directamente, simplemente le decimos a React como queremos que se vea la vista dependiendo de el estado. Podemos pensar del virtual DOM como un objeto de JavaScript que describe nuestra aplicación. El virtual DOM es como un árbol con nodos que contiene los planos que usa React para determinar la forma mas eficiente de actualizar la vista, el DOM, de la pagina.
1 Estado y el DOM Virtual
React utiliza un patrón que se conoce como unidirectional data flow (Flujo de datos unidireccional) en el que el estado solo se puede mover de un nodo padre a nodos que se encuentran en niveles mas abajo de el árbol en nuestra aplicación. Esto nos introduce la primera limitación:
- El estado nunca se puede mover hacia arriba.
Esto implica que si tenemos que compartir estado entre componentes que están en diferentes ramas, tenemos que mover el estado a el nodo mas alto en el árbol que tenga una rama relacionada con ambos componentes que necesitan ese estado. Este patrón es conveniente porque nos permite saber que si estamos teniendo un problema, este radica en el componente en el que vive el estado, pero es problemático cuando compartimos estado entre componentes dispersos en nuestra aplicación. La segunda limitación es de desempeño:
2 Renderizacion de Ramas
- Cambios de estado en un nodo causa la renderizacion de todos los nodos por debajo de el.
En el caso de arriba, donde subimos el estado al nodo común, para tener la vista que queremos tenemos que re-renderizar ambas ramas de la aplicación para solo alterar un componente pequeño bien abajo en el árbol. Esto nos lleva a la tercera limitación y es un anti patrón de React conocido como prop drilling (perforación de props).
3 Prop Drilling
- Prop Drilling dificulta el desarrollo
En este diagrama podemos ver donde debe vivir el estado para que se pueda compartir entre componentes. También podemos ver todos los nodos que se deben renderizar para llevar los cambios a donde los necesitamos. Y podemos ver por donde deben pasar los props para llegar a donde se necesitan. La parte de los props es la mas importante porque esos valores tienen que pasar por varios componentes a los que no les interesan esos valores e introduce la posibilidad de que ocurra algún error en los props en el trayecto.
Lo mas perjudicial de este patrón es que cuando tienes que rastrear un problema en un componente tienes que seguirle el rastro hasta donde reside, y después hacer el rastro hasta donde este estado se puede cambiar dentro de cualquier componente hijo de esa rama. Esto genera un problema a lo que React trató de solucionar, la volatilidad del patrón MVC en las interfaces web.
Patrón MVC
El patrón MVC (Model View Controller) es uno de los patrones mas usados en la programación y varias librerias como NodeJS, JQuery, y AngularJS (Antes de Angular 2). Se compone por 3 partes: El modelo, el controlador, y la vista. El modelo es toda la parte que tiene que ver con los datos, el controlador es la parte que ejecuta la lógica y el intermediario entre el modelo y la vista, y la vista es el resultado de la interacción entre el controlador con el modelo:
Tiene varias presentaciones y variaciones pero el concepto se preserva, y es que el controlador se encarga de los cambios y cada acción tiene su controlador que se encarga de mediar y entregar el resultado final, que en algunos casos puede disparar otras acciones para activar otros controladores si son necesarios para lograr la vista final.
Patron Flux
En frameworks como React nosotros no controlamos la forma en la que queremos que se den los cambios en nuestra vista, nosotros simplemente cambiamos el estado y este refleja los cambios. El patron Flux toma todas las acciones las pasa por un reducer unico que se encarga de actualizar un store unico (Estado) el cual genera el resultado en nuestra vista:
MVC vs Flux
Con el patrón MVC tenemos micro estados que van a cambiar dependiendo de la acción y el controlador y hay que estar pendientes de estas interacciones para cambiar todos los micro estados de tal forma que logramos nuestro resultado final. Algo similar es lo que termina sucediendo con el api común de react (Patron MVVM que es una variacion de MVC), donde cada el estado puede es independiente y nos toca re ubicarlo si queremos compartirlo al igual que pasar el estado y las acciones por componentes a los cuales no les interesa ese estado.
Con el patrón Flux tenemos un solo estado con una forma de cambiarlo: a través de acciones que se procesan en nuestro reducer. Si hay un problema en nuestra vista, el problema tienen que radicar en uno de dos sitios, la acción, o el reducer. Por lo cual fue el patrón que domino en React para el manejo de estado en grandes aplicaciones.
Librerías de manejo de Estado
Usar una librería de manejo de estado con React se volvió algo como usar NodeJS con Express, o Ruby con Rails porque en ultmas React no es un framework, es una librería que es flexible a varias herramientas y estilos y una de las necesidades básicas para React es manejar el estado. El API de React ha evolucionado dándonos el Context API que es como tener varios mini Redux al mismo tiempo.
Redux sigue siendo la librería de manejo de estado mas utilizada en React a pesar de las alternativas que han salido y la critica que le han hecho desde hooks y context. Esto hace que Redux tenga un ecosistema de herramientas y complementos probados en masa que nos dan mucha utilidad con mucha facilidad y que sabemos que funcionan consistentemente. Siendo muy generales, Redux sigue siendo la herramienta mas versátil para manejo de estado consumiendo APIs REST y Apollo Client tiene su propia librería de manejo de estado optimizada para consumir APIs de GraphQL.
Redux
Redux tiene 3 principios:
- Única fuente de verdad (Single source of truth)
- El estado es read-only, inmutable.
- Los cambios se hacen con funciones puras.
Única Fuente de Verdad
Hay un solo objeto que describe el estado de nuestra aplicación. Como React usa el estado para determinar como se va a dar la vista, es muy conveniente saber donde reside ese estado para determinar que es lo que puede estar causando un comportamiento inesperado porque solo se tiene un lugar para mirar.
El estado es Read-only
El objeto que creamos en el store de Redux no se modifica y se basa en el principio de inmutabilidad. Esto es particularmente bueno en React porque nunca vamos a tener problemas de sincronizacion entre diferentes componentes que dependen de los valores del mismo estado porque todos van a estar utilizando el mismo objeto. Este patrón se adopto en React y si uno muta estado directamente puede que no se cause una re-renderizacion porque en los ojos de JavaScript no se produjo un cambio en el estado porque nunca cambio su referencia:
// javascript con valores no primitivos
const arr1 = [1, 2, 3];
const arr2 = [1, 2, 3];
arr1 === arr2; // false porque la comparacion de objetos en js es por referencia.
const arr3 = arr1; // js asigna objetos por referencia
arr3.push(4);
console.log(arr1); // [1,2,3,4]
arr1 === arr3; // true
const arr4 = [...arr2]; // [1,2,3]
arr4 === arr2; // false porque se crea un nuevo objeto con nueva referencia
Cambios con funciones Puras
Una función pura es una función que no tiene efectos secundarios. Esto quiere decir que la función no depende de valores externos para darnos un resultado, esto hace que una función pura siempre retorna el mismo valor dados los mismos argumentos. Esto permite que hagamos cosas como memoizar/cachear los resultados de las funciones para no tener que re ejecutar funciones que reciben los mismos argumentos, lo cual se puede hacer con middleware de Redux con pocas lineas de código.
Dato curioso: Redux y React toman muchos patrones de el paradigma de programación funcional, es por eso que en React los componentes funcionales eran exclusivamente presentacionales (sin estado) hasta React Hooks en el 2018. Tambien, por este paradigma funcional el hook useEffect toma su nombre, pues se encarga de los efectos secundarios (side effects) que hacen que la función dependa de valores por fuera de su alcanze/scope/cierre/closure.
Renderizacion con Redux
Con Redux, como el estado vive por fuera del flujo de nuestros componentes, podemos enviarlo solamente en los componentes que lo necesitan. De esta forma no tenemos que subir el estado a el nodo padre común y pasarlo por todos los componentes que no lo necesitan. Adicionalmente, como el estado no cambia sino en estos componentes, no se tienen que renderizar componentes que en los que no cambio el estado.
Cambios de estado con Redux
Ya vimos que para cambiar el estado necesitamos utilizar acciones, lo bueno es que de la misma forma en la que podemos acceder al estado desde cualquier parte de nuestra aplicación, también podemos acceder a cualquier acción lo cual nos permite generar cambios en el estado sin que el estado que se esta cambiando tenga que estar en el componente.
Porciones del Estado
Si uno necesita porciones mas pequeñas del estado, estas se eligen a través de selectores. Los selectores simplemente son funciones puras que retornan porciones procesadas del estado. Esto abstrae la lógica de nuestros componentes lo que nos permite que nuestros componentes sean presentacionales de nuevo y tener separación de intereses (separation of concerns) que facilita organizar y hacer pruebas en nuestra aplicación. Adicionalmente, como estos selectores son funciones puras, también pueden ser memoizados para no causar renderizaciones innecesarias si la porción del estado que elegimos no cambia.
Conclusión
Redux es la forma mas popular de complementar el manejo de estado en la librería de React. Con sus patrones y principios nos permite manejar el estado en aplicaciones grandes de una forma eficiente, predecible, y estandarizada. No solo nos brinda ventajas en desempeño al limitar la renderizacion a los puntos donde es necesario, sino que nos facilita el desarrollo al limitar los puntos donde tenemos que mirar cuando tenemos comportamiento inesperado en nuestra aplicación. Esto ha sido clave para su popularidad, y esta popularidad ha creado un ecosistema rico que nos permite tener funcionalidad adicional con facilidad a través de middleware como debugging, memoizacion, persistencia en browsers, integraciones con axios, entre otras. Ya que entendemos mejor el problema que soluciona Redux y sus patrones y conceptos, podemos pasar a ver como implementarlo en código.