Recoil introdujo el modelo atómico en el mundo React . Sus nuevos poderes llegaron a costa de una curva de aprendizaje empinada y recursos de aprendizaje escasos.
Jotai y Zedux luego simplificaron varios aspectos de este nuevo modelo, ofreciendo muchas características nuevas y superando los límites de este nuevo y asombroso paradigma.
Otros artículos se centrarán en las diferencias entre estas herramientas. Este artículo se centrará en una gran característica que los 3 tienen en común:
Arreglaron Flux.
Si no conoce Flux, aquí hay una idea general:
Además de Redux , todas las bibliotecas basadas en Flux básicamente siguieron este patrón: una aplicación tiene varias tiendas. Solo hay un Despachador cuyo trabajo es enviar acciones a todas las tiendas en el orden correcto. Este "orden correcto" significa clasificar dinámicamente las dependencias entre las tiendas.
Por ejemplo, tome la configuración de una aplicación de comercio electrónico:
Cuando el usuario mueve, por ejemplo, una banana a su carrito, PromosStore debe esperar a que se actualice el estado de CartStore antes de enviar una solicitud para ver si hay un cupón de banana disponible.
O tal vez los plátanos no se pueden enviar al área del usuario. CartStore necesita verificar UserStore antes de actualizar. O tal vez los cupones solo se pueden usar una vez a la semana. PromosStore necesita verificar UserStore antes de enviar la solicitud de cupón.
A Flux no le gustan estas dependencias. De los documentos heredados de React :
Los objetos dentro de una aplicación Flux están altamente desacoplados y se adhieren muy fuertemente a la Ley de Deméter , el principio de que cada objeto dentro de un sistema debe saber lo menos posible sobre los otros objetos en el sistema.
La teoría detrás de esto es sólida. 100%. Entonces... ¿por qué murió este sabor multitienda de Flux?
Resulta que las dependencias entre contenedores de estados aislados son inevitables. De hecho, para mantener el código modular y SECO, debe usar otras tiendas con frecuencia.
En Flux, estas dependencias se crean sobre la marcha:
// This example uses Facebook's own `flux` library PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } }) CartStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for UserStore to update first: dispatcher.waitFor([UserStore.dispatchToken]) if (UserStore.canBuy(payload.item)) { CartStore.addItem(payload.item) } } })
Este ejemplo muestra cómo las dependencias no se declaran directamente entre tiendas, sino que se ensamblan por acción. Estas dependencias informales requieren excavar a través del código de implementación para encontrarlas.
¡Este es un ejemplo muy simple! Pero ya puedes ver cómo se siente Flux atolondrado. Los efectos secundarios, las operaciones de selección y las actualizaciones de estado se improvisan. Esta colocación en realidad puede ser algo agradable. Pero mezcle algunas dependencias informales, triplique la receta y sírvala en un plato estándar y verá que Flux se descompone rápidamente.
Otras implementaciones de Flux como Flummox y Reflux mejoraron la experiencia repetitiva y de depuración. Si bien era muy útil, la administración de dependencias era el único problema persistente que afectaba a todas las implementaciones de Flux. Usar otra tienda se sintió feo. Los árboles de dependencia profundamente anidados eran difíciles de seguir.
Esta aplicación de comercio electrónico podría algún día tener tiendas para OrderHistory, ShippingCalculator, DeliveryEstimate, BananasHoarded, etc. Una aplicación grande fácilmente podría tener cientos de tiendas. ¿Cómo se mantienen actualizadas las dependencias en cada tienda? ¿Cómo rastreas los efectos secundarios? ¿Qué pasa con la pureza? ¿Qué pasa con la depuración? ¿Son los plátanos realmente una baya?
En cuanto a los principios de programación introducidos por Flux, el flujo de datos unidireccional resultó ganador, pero, por ahora, la Ley de Demeter no lo fue.
Todos sabemos cómo Redux entró rugiendo para salvar el día. Abandonó el concepto de múltiples tiendas a favor de un modelo único. Ahora todo puede acceder a todo lo demás sin ninguna "dependencia" en absoluto.
Los reductores son puros, por lo que toda la lógica que se ocupa de múltiples segmentos de estado debe salir de la tienda. La comunidad hizo estándares para manejar los efectos secundarios y el estado derivado. Las tiendas Redux se pueden depurar maravillosamente. El único defecto importante de Flux que Redux originalmente no solucionó fue su repetitivo.
RTK luego simplificó el infame texto modelo de Redux. Luego, Zustand eliminó algunas pelusas a costa de algo de poder de depuración. Todas estas herramientas se han vuelto extremadamente populares en el mundo de React.
Con el estado modular, los árboles de dependencia se vuelven tan naturalmente complejos que la mejor solución que se nos ocurrió fue: "Simplemente no lo hagas, supongo".
¡Y funcionó! Este nuevo enfoque de singleton todavía funciona lo suficientemente bien para la mayoría de las aplicaciones. Los principios de Flux eran tan sólidos que simplemente eliminando la pesadilla de la dependencia se arregló.
¿O lo hizo?
El éxito del enfoque singleton plantea la pregunta: ¿a qué se refería Flux en primer lugar? ¿Por qué alguna vez quisimos múltiples tiendas?
Permítanme arrojar algo de luz sobre esto.
Con múltiples tiendas, las partes del estado se dividen en sus propios contenedores modulares autónomos. Estas tiendas se pueden probar de forma aislada. También se pueden compartir fácilmente entre aplicaciones y paquetes.
Estas tiendas autónomas se pueden dividir en fragmentos de código separados. En un navegador, se pueden cargar de forma diferida y conectarse sobre la marcha.
Los reductores de Redux también son bastante fáciles de dividir en código. Gracias a replaceReducer
, el único paso adicional es crear el nuevo reductor combinado. Sin embargo, es posible que se requieran más pasos cuando se trata de efectos secundarios y middleware.
Con el modelo singleton, es difícil saber cómo integrar el estado interno de un módulo externo con el tuyo. La comunidad de Redux introdujo el patrón Ducks como un intento de resolver esto. Y funciona, a costa de un poco de repetitivo.
Con múltiples tiendas, un módulo externo simplemente puede exponer una tienda. Por ejemplo, una biblioteca de formularios puede exportar un FormStore. La ventaja de esto es que el estándar es "oficial", lo que significa que es menos probable que las personas creen sus propias metodologías. Esto conduce a una comunidad y un ecosistema de paquetes más sólidos y unificados.
El modelo singleton es sorprendentemente eficaz. Redux lo ha demostrado. Sin embargo, su modelo de selección tiene especialmente un límite superior estricto. Escribí algunos pensamientos sobre esto en esta discusión de Reselección . Un árbol selector grande y costoso puede realmente comenzar a arrastrarse, incluso cuando se toma el máximo control sobre el almacenamiento en caché.
Por otro lado, con varias tiendas, la mayoría de las actualizaciones de estado están aisladas en una pequeña parte del árbol de estado. No tocan nada más en el sistema. Esto es escalable mucho más allá del enfoque de singleton; de hecho, con varias tiendas, es muy difícil alcanzar las limitaciones de la CPU antes de alcanzar las limitaciones de la memoria en la máquina del usuario.
Destruir el estado no es demasiado difícil en Redux. Al igual que en el ejemplo de división de código, solo se requieren unos pocos pasos adicionales para eliminar una parte de la jerarquía de reducción. Pero aún es más simple con varias tiendas: en teoría, simplemente puede separar la tienda del despachador y permitir que se recolecte la basura.
Este es el problema grande que Redux, Zustand y el modelo singleton en general no manejan bien. Los efectos secundarios están separados del estado con el que interactúan. La lógica de selección está separada de todo. Si bien Flux de varias tiendas quizás estaba demasiado ubicado, Redux se fue al extremo opuesto.
Con múltiples tiendas autónomas, estas cosas naturalmente van juntas. Realmente, Flux solo carecía de algunos estándares para evitar que todo se convirtiera en una mezcolanza desordenada de galimatías (lo siento).
Ahora, si conoce la biblioteca OG Flux, sabe que en realidad no fue excelente en ninguno de estos. El despachador sigue adoptando un enfoque global: despachando cada acción a cada tienda. Todo el asunto con las dependencias informales/implícitas también hizo que la división y destrucción del código fuera menos que perfecta.
Aún así, Flux tenía muchas características geniales a su favor. Además, el enfoque de múltiples tiendas tiene potencial para incluso más características como Inversion of Control y administración de estado fractal (también conocido como local).
Flux podría haberse convertido en un administrador estatal verdaderamente poderoso si alguien no hubiera llamado a su diosa Deméter. ¡Lo digo en serio! ... Está bien, no lo soy. Pero ahora que lo mencionas, tal vez la ley de Deméter merece una mirada más cercana:
¿Qué es exactamente esta llamada "ley"? De Wikipedia :
- Cada unidad debe tener solo un conocimiento limitado sobre otras unidades: solo unidades "estrechamente" relacionadas con la unidad actual.
- Cada unidad solo debe hablar con sus amigos; no hables con extraños
Esta ley se diseñó teniendo en cuenta la programación orientada a objetos, pero se puede aplicar en muchas áreas, incluida la gestión del estado de React.
La idea básica es evitar que una tienda:
En términos de plátano, un plátano no debe pelar otro plátano y no debe hablar con un plátano en otro árbol. Sin embargo, puede hablar con el otro árbol si los dos árboles instalan primero una línea telefónica tipo banana.
Esto fomenta la separación de preocupaciones y ayuda a que su código se mantenga modular, SECO y SÓLIDO. ¡Teoría sólida! Entonces, ¿qué se estaba perdiendo Flux?
Bueno, las dependencias entre tiendas son una parte natural de un buen sistema modular. Si una tienda necesita agregar otra dependencia, debe hacerlo y hacerlo de la manera más explícita posible . Aquí hay algo de ese código Flux nuevamente:
PromosStore.dispatchToken = dispatcher.register(payload => { if (payload.actionType === 'add-to-cart') { // wait for CartStore to update first: dispatcher.waitFor([CartStore.dispatchToken]) // now send the request sendPromosRequest(UserStore.userId, CartStore.items).then(promos => { dispatcher.dispatch({ actionType: 'promos-fetched', promos }) }) } if (payload.actionType === 'promos-fetched') { PromosStore.setPromos(payload.promos) } })
PromosStore tiene múltiples dependencias declaradas de diferentes maneras: espera y lee de CartStore
y lee de UserStore
. La única forma de descubrir estas dependencias es buscar tiendas en la implementación de PromosStore.
Las herramientas de desarrollo tampoco pueden ayudar a que estas dependencias sean más detectables. En otras palabras, las dependencias son demasiado implícitas.
Si bien este es un ejemplo muy simple y artificial, ilustra cómo Flux malinterpretó la Ley de Deméter. Si bien estoy seguro de que nació principalmente del deseo de mantener pequeñas las implementaciones de Flux (¡la gestión de dependencia real es una tarea compleja!), Aquí es donde Flux se quedó corto.
A diferencia de los héroes de esta historia:
En 2020, Recoil apareció dando tumbos en escena. Aunque un poco torpe al principio, nos enseñó un nuevo patrón que revivió el enfoque de múltiples tiendas de Flux.
El flujo de datos unidireccional se movió desde la propia tienda al gráfico de dependencia. Las tiendas ahora se llamaban átomos. Los átomos eran propiamente autónomos y se podían dividir en código. Tenían nuevos poderes como soporte de suspenso e hidratación. Y lo más importante, los átomos declaran formalmente sus dependencias.
Nació el modelo atómico.
// a Recoil atom const greetingAtom = atom({ key: 'greeting', default: 'Hello, World!', })
Recoil luchó con una base de código inflada, fugas de memoria, mal rendimiento, desarrollo lento y características inestables, sobre todo efectos secundarios. Lentamente resolvería algunos de estos, pero mientras tanto, otras bibliotecas tomaron las ideas de Recoil y las siguieron.
Jotai irrumpió en escena y rápidamente ganó seguidores.
// a Jotai atom const greetingAtom = atom('Hello, World!')
Además de ser una pequeña fracción del tamaño de Recoil, Jotai ofreció un mejor rendimiento, API más elegantes y sin pérdidas de memoria debido a su enfoque basado en WeakMap.
Sin embargo, tuvo el costo de algo de potencia: el enfoque de WeakMap dificulta el control de la memoria caché y hace que compartir el estado entre varias ventanas u otros reinos sea casi imposible. Y la falta de claves de cadena, aunque elegante, hace que la depuración sea una pesadilla. La mayoría de las aplicaciones deberían volver a agregarlas, empañando drásticamente la elegancia de Jotai.
// a (better?) Jotai atom const greetingAtom = atom('Hello, World!') greetingAtom.debugLabel = 'greeting'
Algunas menciones de honor son Reatom y Nanostores . Estas bibliotecas han explorado más de la teoría detrás del modelo atómico e intentan llevar su tamaño y velocidad al límite.
El modelo atómico es rápido y escala muy bien. Pero hasta hace muy poco, había algunas preocupaciones que ninguna biblioteca atómica había abordado muy bien:
La curva de aprendizaje. Los átomos son diferentes . ¿Cómo hacemos que estos conceptos sean accesibles para los desarrolladores de React?
Dev X y depuración. ¿Cómo hacemos que los átomos sean reconocibles? ¿Cómo realiza un seguimiento de las actualizaciones o aplica las buenas prácticas?
Migración incremental para bases de código existentes. ¿Cómo se accede a las tiendas externas? ¿Cómo se mantiene intacta la lógica existente? ¿Cómo evitar una reescritura completa?
Complementos. ¿Cómo hacemos extensible el modelo atómico? ¿ Puede manejar todas las situaciones posibles?
Inyección de dependencia. Los átomos definen dependencias de forma natural, pero ¿pueden intercambiarse durante las pruebas o en diferentes entornos?
La Ley de Deméter. ¿Cómo ocultamos los detalles de implementación y evitamos actualizaciones dispersas?
Aquí es donde entro yo. Mira, soy el principal creador de otra biblioteca atómica:
Zedux finalmente entró en escena hace unas semanas. Desarrollado por una empresa Fintech en Nueva York, la empresa para la que trabajo, Zedux no solo fue diseñado para ser rápido y escalable, sino también para proporcionar una experiencia de desarrollo y depuración elegante.
// a Zedux atom const greetingAtom = atom('greeting', 'Hello, World!')
No profundizaré en las características de Zedux aquí; como dije, este artículo no se centrará en las diferencias entre estas bibliotecas atómicas.
Baste decir que Zedux aborda todas las preocupaciones anteriores. Por ejemplo, es la primera biblioteca atómica que ofrece una inversión de control real y la primera que nos devuelve el círculo completo a la Ley de Deméter al ofrecer exportaciones atómicas para ocultar detalles de implementación.
Las últimas ideologías de Flux finalmente han sido revividas, ¡no solo revividas, sino mejoradas! - gracias al modelo atómico.
Entonces, ¿qué es exactamente el modelo atómico?
Estas bibliotecas atómicas tienen muchas diferencias, incluso tienen diferentes definiciones de lo que significa "atómico". El consenso general es que los átomos son contenedores de estado pequeños, aislados y autónomos que se actualizan reactivamente a través de un gráfico acíclico dirigido.
Lo sé, lo sé, suena complejo, pero espera a que te lo explique con plátanos.
¡Estoy bromeando! En realidad es muy simple:
Las actualizaciones rebotan a través del gráfico. ¡Eso es todo!
El punto es que, independientemente de la implementación o la semántica, todas estas bibliotecas atómicas han revivido el concepto de tiendas múltiples y las han hecho no solo utilizables, sino también un verdadero placer trabajar con ellas.
Las 6 razones que di para querer varias tiendas son exactamente las razones por las que el modelo atómico es tan poderoso:
Las API simples y la escalabilidad por sí solas hacen que las bibliotecas atómicas sean una excelente opción para cada aplicación React. ¿Más potencia y menos repetitivo que Redux? ¿Es esto un sueño?
¡Qué viaje! El mundo de la administración de estado de React nunca deja de sorprender, y estoy muy contento de haber hecho autostop.
Recién estamos comenzando. Hay mucho espacio para la innovación con los átomos. Después de pasar años creando y usando Zedux, he visto cuán poderoso puede ser el modelo atómico. De hecho, su poder es su talón de Aquiles:
Cuando los desarrolladores exploran los átomos, a menudo profundizan tanto en las posibilidades que regresan diciendo: "Mira este loco y complejo poder", en lugar de "Mira cuán simple y elegantemente los átomos resuelven este problema". Estoy aquí para cambiar esto.
El modelo atómico y la teoría detrás de él no se han enseñado de una manera que sea accesible para la mayoría de los desarrolladores de React. En cierto modo, la experiencia de los átomos en el mundo React hasta ahora ha sido lo opuesto a Flux:
Este artículo es el segundo de una serie de recursos de aprendizaje que estoy produciendo para ayudar a los desarrolladores de React a comprender cómo funcionan las bibliotecas atómicas y por qué es posible que desee usar una. Consulte el primer artículo: escalabilidad: el nivel perdido de la gestión de estado de React .
Tomó 10 años, pero la sólida teoría CS introducida por Flux finalmente está impactando en gran medida a las aplicaciones React gracias al modelo atómico. Y lo seguirá haciendo durante los próximos años.