Está en la página 1de 265

Arquitectura Flux v. 0.

0
Índice
Prefacio.................................................................................................................................................1
Lo que cubre este libro.........................................................................................................................1
Lo que necesitas para este libro............................................................................................................2
Para quién es este libro.........................................................................................................................2
1............................................................................................................................................................3
¿Qué es Flux?.......................................................................................................................................3
Flux es un conjunto de patrones......................................................................................................3
Puntos de entrada de datos..........................................................................................................3
Gestión de estados.......................................................................................................................4
Manteniendo las modificaciones sincronizadas..........................................................................4
Arquitectura de información.......................................................................................................5
Flux no es otro framework...............................................................................................................6
Flux resuelve problemas conceptuales............................................................................................6
Dirección del flujo de datos........................................................................................................7
Causa predecible..............................................................................................................................8
Notificaciones consistentes.........................................................................................................8
Capas arquitectónicas simples.....................................................................................................9
Renderizado poco acoplado......................................................................................................10
Componentes de Flux....................................................................................................................11
Action........................................................................................................................................11
Dispatcher..................................................................................................................................11
Store..........................................................................................................................................12
Vista...........................................................................................................................................13
Instalando el paquete Flux.............................................................................................................14
Sumario..........................................................................................................................................16
2..........................................................................................................................................................17
Principios de Flux...............................................................................................................................17
Desafíos con el MV*.....................................................................................................................17
Separación de responsabilidades...............................................................................................17
Modificaciones en cascada........................................................................................................19
Responsabilidad de actualización del modelo...........................................................................20
Datos unidireccionales...................................................................................................................21
De principio a fin......................................................................................................................21
Sin efectos secundarios.............................................................................................................22
Explícito sobre implícito................................................................................................................22
Modificaciones mediante efectos secundarios ocultos.............................................................23
Los datos cambian de estado en un solo lugar..........................................................................24
¿Demasiadas actions?................................................................................................................25
Capas sobre jerarquías...................................................................................................................25
Múltiples jerarquías de componentes........................................................................................25
Profundidad de jerarquía y efectos secundarios........................................................................26
Flujo de datos y capas...............................................................................................................27
Datos de aplicación y estado UI....................................................................................................27
Más de lo mismo.......................................................................................................................27
Transformaciones fuertemente acopladas.................................................................................28
Centrado en las funcionalidades................................................................................................28
Sumario..........................................................................................................................................29
3..........................................................................................................................................................30
Construyendo una arquitectura esqueleto...........................................................................................30
Organización general.....................................................................................................................30
Estructura de directorios...........................................................................................................30
Gestión de dependencias...........................................................................................................31
Diseño de la información...............................................................................................................32
Los usuarios no entienden los modelos.....................................................................................32
Stores mapean de lo que ve el usuario......................................................................................33
¿Con qué tenemos que trabajar?...............................................................................................33
Puesta en marcha de las stores.......................................................................................................34
Obtención de datos de API........................................................................................................34
Cambiar el estado de los recursos de la API.............................................................................39
Actions locales..........................................................................................................................42
Stores y dominios...........................................................................................................................45
Identificación de alto nivel........................................................................................................45
Datos de la API no relevantes...................................................................................................45
Estructuración de datos de store................................................................................................47
Vistas básicas.................................................................................................................................47
Búsqueda de datos perdidos......................................................................................................48
Identificación de actions...........................................................................................................48
Escenarios de extremo a extremo..................................................................................................50
Lista de actions..........................................................................................................................50
Lista de store.............................................................................................................................50
Lista de vista.............................................................................................................................50
Sumario..........................................................................................................................................51
4..........................................................................................................................................................52
Creando Actions.................................................................................................................................52
Nombres y constantes de actions...................................................................................................52
Convenciones de nombres de acciones.....................................................................................52
Datos estáticos de action...........................................................................................................53
Organizar constantes de action..................................................................................................55
Creadores de acciones....................................................................................................................56
Cuando se necesita modularidad...............................................................................................56
Arquitectura modular................................................................................................................57
Simulación de datos.......................................................................................................................58
Simulación de APIs existentes..................................................................................................58
Simulando nuevas APIs............................................................................................................59
Sustitución de los creadores de acciones..................................................................................62
Creadores de actions con estado....................................................................................................63
Integración con otros sistemas..................................................................................................64
Conectividad de web socket......................................................................................................64
Creadores de actions parametrizables............................................................................................67
Eliminación de actions redundantes..........................................................................................67
Mantener las actions genéricas..................................................................................................67
Creación de actions parciales....................................................................................................70
Sumario..........................................................................................................................................71
5..........................................................................................................................................................72
Actions asíncronas..............................................................................................................................72
Manteniendo Flux sincrónico........................................................................................................72
¿Por qué sincronicidad?............................................................................................................72
Encapsulación del comportamiento asíncrono..........................................................................73
de acción asíncrona..................................................................................................................74
Realización de llamadas a API.......................................................................................................76
Las APIs son el caso común......................................................................................................76
Llamadas a la API e interactividad con el usuario....................................................................77
Combinando llamadas API............................................................................................................80
Creadores de acciones complejas..............................................................................................80
Componer creadores de acciones..............................................................................................83
Devolviendo promesas...................................................................................................................84
Sincronizar sin promesas..........................................................................................................84
Componiendo comportamientos asíncronos.............................................................................85
Manejo de errores......................................................................................................................87
Sumario..........................................................................................................................................89
6..........................................................................................................................................................90
Cambiando el estado de la store en Flux............................................................................................90
Adaptación a la información cambiante........................................................................................90
Cambiar los datos de la API......................................................................................................90
Modificar las características de una funcionalidad...................................................................91
Componentes impactados.........................................................................................................91
Reducir los datos duplicados en store............................................................................................92
Datos de store generica.............................................................................................................92
Registro de stores genéricas......................................................................................................93
Combinación de datos genéricos y específicos.........................................................................96
Gestión de dependencias entre stores............................................................................................98
A la espera de las stores............................................................................................................99
Dependencias de datos............................................................................................................101
Dependencias de la interfaz de usuario...................................................................................101
Ver orden de actualización...........................................................................................................105
Orden de registro de la store...................................................................................................106
Priorización en la renderización de vistas...............................................................................106
Lidiando con la complejidad de la store......................................................................................107
Demasiadas stores...................................................................................................................107
Repensar los dominios de las funcionalidades........................................................................107
Sumario........................................................................................................................................108
7........................................................................................................................................................109
Viendo información..........................................................................................................................109
Pasar los datos de las vistas.........................................................................................................109
Datos mediante un evento de modificación............................................................................109
Las vistas deciden cuándo renderizar......................................................................................112
Mantener las vistas sin estado......................................................................................................114
El estado de la UI pertenece a las stores.................................................................................114
Sin necesidad de consultar el DOM........................................................................................114
Ver responsabilidades...................................................................................................................115
Renderización de datos de store..............................................................................................115
Estructura de vista parcial.......................................................................................................116
Interactividad con el usuario...................................................................................................117
Uso de ReactJS con Flux.............................................................................................................117
Ajustado del estado de la vista................................................................................................117
Componer vistas......................................................................................................................121
Reaccionando a los eventos....................................................................................................123
Enrutamiento y acciones.........................................................................................................125
Sumario........................................................................................................................................129
8........................................................................................................................................................130
Ciclo de vida de la información........................................................................................................130
Dificultades en el ciclo de vida de los componentes...................................................................130
Recuperación de recursos no utilizados..................................................................................130
Dependencias ocultas..............................................................................................................131
Fugas de memoria...................................................................................................................132
Las estructuras de Flux son estáticas...........................................................................................132
Patrón Singleton......................................................................................................................133
Comparación con los modelos................................................................................................135
Vistas estáticas........................................................................................................................136
Escalado de información..............................................................................................................139
¿Qué escala bien?....................................................................................................................139
Información mínima requerida................................................................................................142
Acciones que escalan..............................................................................................................142
Stores inactivas............................................................................................................................143
Borrar datos de store...............................................................................................................143
Optimización de stores inactivas.............................................................................................146
Actualización de datos en store...............................................................................................146
Sumario........................................................................................................................................153
9........................................................................................................................................................154
Stores inmutables..............................................................................................................................154
Renunciar a actualizaciones ocultas............................................................................................154
Cómo romper el Flux..............................................................................................................154
Obtención de datos de un store...............................................................................................156
Todo es inmutable...................................................................................................................157
Imponer el flujo de datos unidireccional.....................................................................................158
Flujo de datos hacia atrás, hacia los lados y con fugas...........................................................158
¿Demasiadas stores?................................................................................................................159
No hay suficientes acciones....................................................................................................159
Hacer cumplir la inmutabilidad...............................................................................................159
El coste de los datos inmutables..................................................................................................164
La recolección de basura es cara.............................................................................................164
Mutaciones por lotes...............................................................................................................164
Compensación del coste..........................................................................................................165
Usando Immutable.js...................................................................................................................166
Listas y mapas inmutables......................................................................................................166
Transformaciones inmutables.................................................................................................169
Detección de cambios.............................................................................................................171
Sumario........................................................................................................................................174
10......................................................................................................................................................175
Implementando un dispatcher...........................................................................................................175
Interfaz abstracta del dispatcher..................................................................................................175
Registro de store......................................................................................................................175
Envío de payloads...................................................................................................................176
Gestión de dependencias.........................................................................................................176
Desafíos con el despachador........................................................................................................177
Propósitos educativos..............................................................................................................177
Dispatchers Singleton.............................................................................................................178
Registro manual de la store.....................................................................................................178
Gestión de dependencias propensas a errores.........................................................................179
Construcción de un módulo de dispatcher...................................................................................179
Encapsulación de referencias de store.....................................................................................179
Gestión de dependencias.........................................................................................................180
Envío de acciones....................................................................................................................182
Mejorar el registro de stores........................................................................................................185
Clase base de store..................................................................................................................185
Un método de acción...............................................................................................................186
Sumario........................................................................................................................................189
11......................................................................................................................................................191
Componentes Vista Alternativos......................................................................................................191
ReactJS se adapta bien a Flux......................................................................................................191
ReactJS es unidireccional........................................................................................................191
Renderizar nuevos datos es fácil.............................................................................................192
Pequeña cantidad de código....................................................................................................193
Las desventajas de ReactJS..........................................................................................................193
Virtual DOM y memoria.........................................................................................................194
JSX y lenguaje de marcas.......................................................................................................195
Bloqueo de proveedor.............................................................................................................195
Uso de jQuery y Handlebars........................................................................................................196
¿Por qué jQuery y Handlebars?...............................................................................................196
Renderización de plantillas.....................................................................................................196
Componer vistas......................................................................................................................199
Manejo de eventos..................................................................................................................201
Usando VanillaJS.........................................................................................................................206
Mantener mis opciones abiertas..............................................................................................206
Mudarse a React......................................................................................................................207
Novedades...............................................................................................................................207
Sumario........................................................................................................................................207
12......................................................................................................................................................209
Aprovechamiento de las librerías Flux.............................................................................................209
Implementación de componentes principales de Flux.................................................................209
Personalización del dispatcher................................................................................................209
Implementación de un store base............................................................................................209
Creación de acciones...............................................................................................................210
Puntos débiles de la aplicación....................................................................................................211
Planificación de acciones asíncronas......................................................................................211
Partición de stores...................................................................................................................211
Usando Alt...................................................................................................................................212
Las ideas centrales...................................................................................................................212
Creación de stores...................................................................................................................212
Declarar creadoras de acción..................................................................................................215
Escuchar los cambios de estado..............................................................................................215
Visualización de vistas y envío de acciones............................................................................216
Usando Redux..............................................................................................................................219
Las ideas centrales...................................................................................................................219
Reductoras y stores.................................................................................................................219
Acciones Redux......................................................................................................................222
Renderizando componentes y enviando acciones...................................................................223
Sumario........................................................................................................................................227
13......................................................................................................................................................228
Pruebas y rendimiento......................................................................................................................228
Hola Jest.......................................................................................................................................228
Probando a las creadoras de acciones..........................................................................................230
Funciones síncronas................................................................................................................230
Funciones asíncronas..............................................................................................................231
Prueba de stores...........................................................................................................................234
Probando los listeners de la store............................................................................................234
Pruebas de condiciones iniciales..................................................................................................236
Metas de rendimiento...................................................................................................................239
Rendimiento percibido por el usuario.....................................................................................239
Rendimiento medido...............................................................................................................240
Requisitos de funcionamiento.................................................................................................240
Herramientas................................................................................................................................241
Acciones asíncronas................................................................................................................241
Memoria de store....................................................................................................................241
Uso de CPU.............................................................................................................................242
Herramientas de benchmarking...................................................................................................242
Código de benchmarking........................................................................................................242
Transformaciones de estado....................................................................................................243
Sumario........................................................................................................................................245
14......................................................................................................................................................246
Flux y el ciclo de vida del desarrollo de software............................................................................246
Flux está abierto a la interpretación.............................................................................................246
Opción de implementación 1 - sólo patrones..........................................................................246
Opción de implementación 2 - usar una librería Flux.............................................................247
Crea tu propio Flux.................................................................................................................247
Metodologías de desarrollo..........................................................................................................248
Actividades iniciales de Flux..................................................................................................248
Maduración de una aplicación Flux........................................................................................249
Tomar ideas prestadas de Flux.....................................................................................................249
Flujo de datos unidireccional..................................................................................................249
El diseño de la información es el rey......................................................................................250
Empaquetando Componentes de Flux.........................................................................................250
Argumentos a favor de Flux monolítico.................................................................................250
Los paquetes permiten escalar................................................................................................251
Componentes Flux instalables................................................................................................251
Sumario........................................................................................................................................258
Prefacio
Me encanta Backbone.js. Es una pequeña biblioteca increíble que hace tanto con tan poco. Es tan
iluminador - hay maneras sin fin de hacer la misma cosa. Esta última condición le da a muchos
programadores de Backbone.js dolor de cabeza. La libertad de aplicar las cosas de la manera que
vemos que encajan es enorme, hasta que empezamos a hacer esos inevitables errores de
consistencia.
Cuando empecé con Flux, realmente no podía ver cómo una arquitectura así podría ayudar a un
simple programador de Backbone.js. Eventualmente, descubrí dos cosas. En primer lugar, Flux no
se pronuncia sobre lo que importa: los detalles de la implementación. Dos, Flux es muy parecido a
Backbone en el espíritu de piezas móviles mínimas para hacer las cosas bien.
A medida que empecé a experimentar con Flux, me di cuenta de que Flux proporciona la
información que faltaba, una perspectiva arquitectónica que permite la escalabilidad, donde
Backbone.js y otras tecnologías relacionadas se desmoronan cuando algo sale mal. De hecho, esos
bugs pueden ser tan complicados que en realidad nunca se arreglan - todo el sistema está parcheado
con soluciones alternativas.
Decidí escribir este libro con la esperanza de que otros programadores, de todo tipo de JavaScript,
puedan experimentar el mismo nivel de iluminación que tengo trabajando con esta maravillosa
tecnología de Facebook.

Lo que cubre este libro


Capítulo 1, ¿Qué es Flux?, da una visión general de lo que es Flux y por qué fue creado.
Capítulo 2, Principios de Flux, habla sobre los conceptos centrales de Flux y el conocimiento
esencial para construir una arquitectura en Flux.
Capítulo 3, Construyendo una Arquitectura Esqueleto, encamina a través de pasos secuenciales, a la
construcción de una arquitectura esqueleto antes de implementar las funcionalidades de la
aplicación.
Capítulo 4, Creación de Actions, muestra cómo se utilizan las funciones actions para incluir nuevos
datos en un sistema describiendo algo que ha ocurrido.
Capítulo 5, Acciones asíncronas, se presentan ejemplos de funciones actions asíncronas y cómo
encajan en una arquitectura Flux.
Capítulo 6, Cambiando el Estado de Stores en Flux, da muchas explicaciones detalladas y ejemplos
que ilustran cómo funcionan el store en Flux.
Capítulo 7, Viendo la Información, da muchas explicaciones detalladas y ejemplos que ilustran
cómo funcionan las vistas en Flux.
Capítulo 8, Ciclo de vida de la información, habla sobre cómo la información en una arquitectura
Flux entra en el sistema y cómo sale finalmente del sistema.
Capítulo 9, Stores Inmutables, muestra cómo la inmutabilidad es una propiedad arquitectónica
clave, de las arquitecturas de software como Flux, donde los datos fluyen en una única dirección.
Capítulo 10, Implementación de un dispatcher, repasa la implementación de un dispatcher usando,
como implementación de referencia, Facebook.
Capítulo 11, Componentes de vista alternativos, muestra cómo implementar tecnologías de vista
distintas a React dentro de una arquitectura Flux.
Capítulo 12, Aprovechamiento de Librerías de Flux, da una visión general de dos librerías
populares de Flux-Alt.js y Redux.
Capítulo 13, Pruebas y rendimiento, habla sobre las pruebas de componentes en el contexto de una
arquitectura Flux y comenta las pruebas de rendimiento en su arquitectura.
Capítulo 14, Flux y el Ciclo de Vida del Desarrollo de Software, discute el impacto que tiene Flux
en el resto del software y cómo empaquetar las funciones de Flux.

Lo que necesitas para este libro


• Cualquier navegador web
• NodeJS >= 4.0
• Un editor de código

Para quién es este libro


¿Tratas de usar React, pero está siendo difícil entender Flux? Tal vez, ¿estás cansado del código
espagueti de el MV* al escalar aplicaciones? ¿Te preguntás ¡Qué es Flux!?
Flux Architecture te guiará a través de todo lo que necesitas entender del patrón y el diseño de Flux,
y a construir poderosas aplicaciones web que usen la arquitectura Flux.
No necesita saber lo que es Flux o cómo funciona para leer el libro. No es necesario el
conocimiento de otras tecnologías adyacentes a Flux, como ReactJS, para seguir adelante, pero se
recomienda tener un buen conocimiento de JavaScript.
1
¿Qué es Flux?
Flux se supone que es esta nueva gran manera de construir interfaces de usuario complejas que
escala bien. Al menos ese es el mensaje general sobre Flux, si sólo estás hojeando la información de
Internet. Pero, ¿cómo definimos esta gran nueva forma de construir interfaces de usuario? ¿Qué lo
hace superior a otras arquitecturas frontend más establecidas?
El objetivo de este capítulo es cortar las fortalizas y escribir explícitamente qué es y qué no es Flux,
observando los patrones que proporciona Flux. Y ya que Flux no es un paquete de software en el
sentido tradicional, repasaremos la sección problemas conceptuales que estamos tratando de
resolver con Flux.
Finalmente, cerraremos el capítulo examinando los componentes centrales que se encuentran en
cualquier arquitectura Flux, e instalaremos el paquete Flux con npm y escribiremos una aplicación
hola mundo. Vamos a empezar.

Flux es un conjunto de patrones


Probablemente deberíamos enfrentarnos a la dura realidad primero -Flux no es un paquete de
software. Es un conjunto de patrones arquitectónicos que debemos seguir. Aunque esto puede sonar
decepcionante para algunos, no desesperes, hay buenas razones para no implementar todavía otro
marco. A lo largo de este libro, veremos el valor de Flux como conjunto de patrones en lugar de una
implementación en si. Por ahora, mostraremos algunos de los patrones arquitectónicos de alto nivel
establecidos por Flux.

Puntos de entrada de datos


Con los enfoques tradicionales de construcción de arquitecturas frontend, no pensamos mucho en la
forma en que los datos entran en el sistema. Podríamos considerar la idea de puntos de entrada de
datos, pero no en detalle. Por ejemplo, con las arquitecturas MVC (Modelo Vista Controlador), se
supone que el controlador controla el flujo de datos. Y en su mayor parte, hace exactamente eso. Por
otro lado, el controlador sólo controla lo que sucede después de que ya tiene los datos. En primer
lugar, ¿cómo el controlador obtiene datos? Considera la siguiente ilustración:

A primera vista, no hay nada malo en esta imagen. El flujo de datos, representado por las flechas, es
fácil de seguir. ¿Pero dónde se originan los datos? Por ejemplo, desde la vista se pueden crear
nuevos datos y pasarlos al controlador, en respuesta a un evento de usuario. Un controlador puede
crear nuevos datos y pasárselos a otro controlador, dependiendo de como esté compuesta nuestra
jerarquía de controladores. ¿Qué pasa con el controlador en cuestión? ¿crea los datos por sí mismo
y luego los usa?
En un diagrama como este, estas preguntas no tienen mucho misterio. Pero, si estamos intentando
escalar una arquitectura para tener cientos de estos componentes, los puntos en el que los datos
entran en el sistema son muy importantes. Dado que Flux se utiliza para construir arquitecturas que
escalen, considera que los puntos de entrada de datos son un factor importante en el patrón
arquitectónico.

Gestión de estados
El estado es una de esas realidades a las que tenemos que hacer frente en el desarrollo frontend.
Desafortunadamente, no podemos componer toda nuestra aplicación de funciones puras sin efectos
secundarios por dos razones. Primero, nuestro código necesita interactuar con la interfaz DOM, de
una forma u otra. Así es como el usuario ve los cambios en la interfaz de usuario. Segundo, no
podemos almacenar todos los datos de nuestra aplicación en el DOM (al menos no deberíamos
hacer esto). Como el tiempo y el usuario interactúan con la aplicación, estos datos cambiarán.
No existe un enfoque simplista para la gestión del estado en una aplicación web, pero existe varias
maneras de limitar la cantidad de cambios de estado que pueden ocurrir y reforzar cómo suceden.
Por ejemplo, las funciones puras no cambian el estado de nada, sólo pueden crear nuevos datos. He
aquí un ejemplo de cómo se ve esto:

Como puedes ver, no hay efectos secundarios con funciones puras porque no hay cambios en los
datos como resultado de llamarlas. Entonces, ¿por qué es este un rasgo deseable, si los cambios de
estado son inevitables? La idea es reforzar donde ocurren los cambios de estado. Por ejemplo, tal
vez sólo permitamos que ciertos tipos de componentes cambien el estado de nuestra aplicación. De
esta manera, podemos descartar varias fuentes como causa de un cambio de estado.
Flux es importante para controlar dónde ocurren los cambios de estado. Avanzando en el capítulo,
veremos cómo los stores de Flux gestionan los cambios de estado. Lo que es importante acerca de
cómo Flux maneja el estado es que se maneja en una capa arquitectónica. Contraste esto con un
enfoque que establece un conjunto de reglas que dicen qué tipos de componentes pueden cambiar
los datos de la aplicación -las cosas se vuelven confusas. Con Flux, hay menos posibilidades para
adivinar dónde se producen los cambios de estado.

Manteniendo las modificaciones sincronizadas


Complementario a los puntos de entrada de datos es conocer la sincronización de modificaciones.
Es decir, además de gestionar de dónde provienen los cambios de estado, tenemos que gestionar el
orden de estos cambios en relación con otras cosas. Si los puntos de entrada de datos son el qué de
nuestros datos, entonces la sincronía de los cambios de estado de nuestra aplicación a través los
datos en nuestro sistema es el cuándo.
Pensemos por un momento por qué esto es importante. En un sistema en el que los datos se
actualizan de forma asíncrona, tenemos que tener en cuenta las condiciones de la carrera. Las
condiciones de carrera pueden ser problemáticas porque un dato puede depender de otro, y si se
actualizan en el orden equivocado, vemos problemas en cascada, de un componente a otro. Echa un
vistazo a este diagrama, que ilustra este problema:

Cuando algo es asincróno, no tenemos control sobre cuándo ese algo cambia de estado. Por lo tanto,
todo lo que podemos hacer es esperar a que se produzcan las actualizaciones asíncronas, y luego
revisar nuestros datos y asegurarnos de que todas nuestras dependencias de datos están satisfechas.
Sin herramientas que manejen automáticamente estas dependencias por nosotros, terminamos
escribiendo mucho código de control de estados.
Flux aborda este problema asegurando que las modificaciones que tienen lugar en nuestros
almacenes de datos estén sincronizadas. Esto significa que el escenario ilustrado en el diagrama
anterior no es posible. Aquí mostramos una mejor visualización de cómo Flux maneja los
problemas de sincronía de datos que son típicos de las aplicaciones JavaScript hoy en día:

Arquitectura de información
Es fácil olvidar que trabajamos en tecnología de la información y que deberíamos estar
construyendo tecnología alrededor de la información. En los últimos años, sin embargo, parece que
hemos cambiado de dirección, donde nos vemos obligados a pensar en la implementación antes de
pensar en la información. La mayoría de las veces, los datos expuestos usados por nuestra
aplicación no ofrecen lo que el usuario necesita. Depende de JavaScript convertir estos datos en
algo consumible para el usuario. Esta es nuestra arquitectura de información.
¿Significa esto que Flux se utiliza para diseñar arquitecturas de información en contraposición a una
arquitectura de software? Este no es el caso en absoluto. De hecho, los componentes de Flux se
realizan como verdaderos componentes de software que realizan cálculos reales. El truco es que los
patrones de Flux nos permiten pensar en la arquitectura de la información como un diseño de
primera clase. En lugar de tener que cribar todo tipo de componentes y su implementación,
podemos asegurarnos de que estamos mostrando la información correcta al usuario.
Una vez que nuestra arquitectura de información toma forma, la arquitectura más grande de nuestra
aplicación sigue, como una extensión natural a la información que estamos tratando de comunicar al
usuario. Producir información a partir de datos es la parte difícil. Tenemos que destilar muchas
fuentes de datos para terminar ofreciendo información útil para el usuario. Hacer esto mal es un
gran riesgo para cualquier proyecto. Cuando lo hagamos bien, podremos pasarla a los componentes
específicos de la apliación, como el estado de un botón, y así sucesivamente.
La arquitectura de Flux mantienen las transformaciones de datos confinadas a sus stores. Un store
es una información en bruto que es procesada y sale nueva información. Los stores controlan cómo
los datos entran en el sistema, la sincronía de los cambios de estado, y definen cómo cambia el
estado. Cuando profundicemos en los stores a medida que progresemos en el libro, veremos cómo
son los pilares de nuestra arquitectura de información.

Flux no es otro framework


Ahora que hemos explorado algunos de los patrones de alto nivel de Flux, es el momento de volver
a retomar la pregunta: ¿qué es Flux? Bueno, es sólo un conjunto de patrones arquitectónicos que
podemos aplicar a nuestras aplicaciones frontend de JavaScript. Flux escala bien porque pone la
información primero. La información es el aspecto más difícil de escalar del software; Flux aborda
la arquitectura de la información de frente.
Entonces, ¿por qué no se implementan los patrones de Flux como un framework? De esta manera,
Flux tiene una implementación regular para que todos la usen; y como cualquier otra
implementación a gran escala de código abierto, el código mejorará con el tiempo a medida que el
proyecto madure.
El principal problema es que Flux opera a nivel arquitectónico. Se utiliza para resolver problemas
de información que impiden que una aplicación determinada se adapte a la demanda de los usuarios.
Si Facebook decidiera lanzar Flux como otro framework JavaScript, probablemente tendría los
mismos tipos de problemas de implementación que existen en otros frameworks. Por ejemplo, si
algún componente de un framework no se implementa de la manera que mejor se adapte al proyecto
en el que estamos trabajando, entonces no es tan fácil implementar una alternativa mejor, sin cortar
el framework en pedacitos.
Lo bueno de Flux es que Facebook decidió dejar las opciones de implementación sobre la mesa.
Proporcionan algunas implementaciones de componentes de Flux, pero éstas son implementaciones
de referencia. Son funcionales, pero la idea es que sean un punto de partida para que entendamos la
mecánica de cómo se espera que funcionen cosas como los dispatcher. Somos libres de implementar
el patrón arquitectónico de Flux como veamos.
Flux no es un framework. ¿Significa esto que tenemos que aplicar todo por nosotros mismos? No,
no lo hacemos. De hecho, los desarrolladores están implementando librerías Flux y liberandolas
como proyectos de código abierto. Algunas librerías Flux se ciñen más estrechamente al patrón de
Flux que otras. Estas implementaciones son dogmáticas y no hay nada erróneo en usarlas si son una
buena opción para lo que estamos construyendo. Flux tiene como objetivo resolver problemas
conceptuales genéricos con el desarrollo de JavaScript, así que aprenderas cuáles son antes de
sumergirte en el proceso de implementación de Flux.

Flux resuelve problemas conceptuales


Si Flux es simplemente una colección de patrones arquitectónicos en lugar framework, ¿qué tipo de
problemas resuelve? En esta sección, veremos algunos de los conceptos que Flux aborda desde una
perspectiva arquitectónica. Estos incluyen el flujo de datos unidireccional; la trazabilidad,
consistencia y estratificación de componentes; y el desacoplamiento de componentes. Cada uno de
estos problemas conceptuales plantea un grado de riesgo para nuestro software, en particular la
posibilidad de escalarlo. Flux nos ayuda a enfrentarnos a estos mientras construimos el software.

Dirección del flujo de datos


Estamos creando una arquitectura de información para dar soporte a una aplicación rica en
funciones que, en última instancia, se situará en la cima de esta arquitectura. Los datos fluyen hacia
el sistema y eventualmente alcanzarán un endpoint, terminando el flujo. Es lo que sucede entre el
punto de entrada y el punto de terminación lo que determina el flujo de datos dentro de una
arquitectura Flux. Esto se ilustra aquí:

El flujo de datos es una abstracción útil, ya que es fácil visualizar los datos a medida que entran. el
sistema y se mueven de un punto a otro. Eventualmente, el flujo se detiene. Pero antes de que lo
haga, se producen varios efectos secundarios en el camino. Es ese punto en el diagrama anterior el
que es preocupante, porque no sabemos exactamente cómo el flujo de datos llegó al final.
Digamos que nuestra arquitectura no plantea ninguna restricción en el flujo de datos. Se permite que
cualquier componente pase datos a cualquier otro componente, sin tener en cuenta de dónde se
encuentra ese componente. Tratemos de visualizar esta configuración:

Como puedes ver, nuestro sistema tiene puntos de entrada y salida claramente definidos para
nuestros datos. Esto es bueno porque significa que podemos decir con confianza que los datos
fluyen a través de nuestro sistema. El problema en esta ilustración es cómo fluyen los datos entre
los componentes del sistema. No hay dirección, o mejor dicho, es multidireccional. Esto no es algo
bueno.
Flux es una arquitectura de flujo de datos unidireccional. Esto significa que la disposición de
componentes anterior no es posible. La pregunta es: ¿por qué importa esto? A veces, puede parecer
conveniente poder pasar datos en cualquier dirección, es decir, de cualquier componente a cualquier
otro componente. Esto en sí mismo no es el problema -pasar los datos por sí solo no rompe nuestra
arquitectura. Sin embargo, cuando los datos se mueven por nuestro sistema en más de una
dirección, hay más oportunidades de que los componentes no estén sincronizados entre sí. Esto
significa simplemente que si los datos no siempre se mueven en la misma dirección, siempre existe
la posibilidad de encontrar errores.
Flux refuerza la dirección de los flujos de datos y, por lo tanto, elimina la posibilidad de que los
componentes se actualicen por sí mismos en un orden que rompa el sistema. No importa qué datos
acaban de entrar en el sistema, siempre fluirán por el sistema en el mismo orden que cualquier otro
dato, como se ilustra aquí:

Causa predecible
Con los datos entrando en nuestro sistema y fluyendo a través de nuestros componentes en una
dirección podemos rastrear más fácilmente cualquier causa por su efecto. Por el contrario, cuando
nos envía datos a cualquier componente que resida en cualquier capa arquitectónica, es mucho más
difícil saber cómo llegaron los datos a su destino. ¿Por qué esto importa? Los depuradores son tan
sofisticados que podemos atajar fácilmente cualquier nivel de complejidad durante el tiempo de
ejecución. El problema con esto es que se supone que sólo necesitamos rastrear lo que está pasando
en nuestro código con el propósito de depurar.
La arquitectura de Flux tiene flujos de datos inherentemente predecibles. Esto es importante para el
diseño y no sólo para la depuración. Los programadores que trabajan en aplicaciones Flux
comenzarán a intuir lo que va a suceder. La anticipación es clave, porque nos permite evitar los
callejones sin salida antes de llegar a ellos. Cuando la causa y el efecto son fáciles de determinar,
podemos dedicar más tiempo a construir las de la aplicación, cosa que preocupa a los clientes.

Notificaciones consistentes
La dirección en la cual pasamos datos de componente a componente en la arquitectura Flux debe de
ser consistente. En términos de consistencia, necesitaríamos pensar sobre el mecanismo usado para
mover datos en nuestro sistema.
Por ejemplo, publicar/suscribir (pub/sub) es un popular mecanismo usado para la comunicación
entre componentes. Lo que es bueno de este enfoque es que nuestros componentes pueden
comunicarse con otros, y aun estamos capacitados de mantener un nivel de desacoplamiento. De
hecho, esto es bastante común en el desarrollo frontend porque la comunicación entre componentes
es en gran medida conducida por los eventos de usuario. Estos eventos pueden ser considerados
guiados. Algunos otros componentes que quieran responder a esos eventos de alguna manera,
necesitan hacerlo por sí mismos suscribiéndose a un evento concreto.
Mientras pub/sub posee algunas buenas propiedades, esto plantea desafíos en la arquitectura, en
particular complejidad en el escalado. Por ejemplo, digamos que hemos añadido una gran cantidad
de nuevos componentes para una nueva funcionalidad. ¿En qué orden esos componentes reciben
mensajes modificados relativos a componentes pre-existentes? ¿Serán notificados después de todos
los componentes pre-existentes? ¿Deberían de serlo primero? Esto presenta una dependencia de
datos en el escalado.
El otro desafío con pub/sub es que los eventos que se publiquen son a menudo refinados hasta el
punto donde querremos subscribir y más tarde rechazar las notificaciones. Esto conduce a desafíos
considerables porque tratar de codificar cambios en el ciclo de vida de alguna parte de nuestra
aplicación cuando hay un gran número de componentes en el sistema es difícil y presenta
posibilidades para que se pierdan eventos.
La idea de Flux es eludir el problema manteniendo una infraestructura estática de inter-
componentes que emite notificaciones a cada componente. En otras palabras, los desarrolladores no
tienen que elegir y escoger los eventos a los que sus componentes están suscritos. En lugar de eso,
tienen que resolver cuales de esos eventos que se envían a ellos son relevantes, ignorando el resto.
Aquí hay una visualización de como Flux envía eventos a los componentes:

El dispatcher de Flux envía el evento a cada componente; inevitablemente. En lugar de tratar de


manipular con la infraestructura de mensajes, la cual dificulta el escalado, implementamos lógica en
los componentes para determinar que mensajes son de interés y cuales no. También capacitamos al
componente declarando dependencias sobre otros, lo cual ayuda en la prioridad del orden de los
mensajes. Cubriremos esto con más detalle en capítulos posteriores.

Capas arquitectónicas simples


Las capas son una buena forma de organizar una arquitectura de componentes. Por un motivo, es
una manera obvia de catalogar los variados componentes que componen nuestra aplicación. Por otro
motivo, las capas sirven como un medio para poner restricciones a nuestras vías de comunicación.
Este último punto es especialmente relevante en la arquitectura Flux dada la importancia del flujo
de datos unidereccional. Es más sencillo aplicar restricciones a las capas que a los componentes de
manera individual. Aquí hay una ilustración de las capas en Flux:
Este diagrama no intenta capturar el flujo de datos completo de la arquitectura
Flux, tan solo el flujo de datos entre las tres capas principales. No nos da ningún
detalle sobre como están compuestas las capas. No te preocupes, la próxima
sección da explicaciones introductorias de los tipos de componentes en Flux, y la
comunicación que ocurre entre las capas es el foco de este libro.
Como ves, el flujo de datos de una capa a la siguiente, es unidireccional. Flux sólo tiene unas pocas
capas, y como el escalado de nuestra aplicación en términos de cantidad de componentes , el
número de capas permanece fijo. Esto pone un tapón en la complejidad implicada al añadir nuevas
a una aplicación grande ya en producción. En suma a la restricción del número de capas y de la
dirección del flujo de datos, la arquitectura Flux es estricta sobre qué capas tienen permitido
comunicarse con otras.
Por ejemplo, la capa action podría comunicarse con la capa view, y estaríamos desplazándonos en
una única dirección. Aún podríamos mantener las capas que Flux necesita. Sin embargo, saltarnos
una capa está prohibido. Para asegurarnos de que las capas sólo se comunican con otra capa
directamente, podemos manejar los errores introducidos haciendo algo cosas fuera de lugar.

Renderizado poco acoplado


Una decisión tomada por los diseñadores de Flux es que la arquitectura Flux no se preocupa de
como los elementos UI son renderizados. Es decir, la capa de la vista está poco acoplada del resto
de la arquitectura. Hay buenas razones para esto.
Flux es una arquitectura de información en primer lugar, y una arquitectura de software en segundo.
Comenzaremos con el primero y gradualmente pasamos al segundo. El desafío con la tecnología de
la vista es que puede ejercer una influencia negativa en el resto de la arquitectura. Por ejemplo, una
vista tiene una forma particular de interaccionar con el DOM. Entonces, si nos decidimos por esa
tecnología, terminaremos influenciando la forma en que nuestra arquitectura de información está
estructurada. Esto no es necesariamente malo, pero puede conducirnos a realizar concesiones sobre
la información que finalmente mostraremos a los usuarios.
Lo que nos debería realmente preocupar es la información por sí misma y como esa información
cambia a lo largo del tiempo. ¿Qué acciones están involucradas en esos cambios? ¿Como una
cantidad de datos depende de otra cantidad de datos? Flux naturalmente se retira de las restricciones
de la tecnología de los navegadores actuales así que podemos dar importancia a la información. Es
fácil conectar vistas en nuestra arquitectura de información a medida de que evoluciona nuestro
software.
Componentes de Flux
En esta sección, comenzaremos nuestro viaje en los conceptos de Flux. Estos conceptos son los
ingredientes esenciales utilizados en la formulación de una arquitectura Flux. Si bien no hay
especificaciones detalladas sobre cómo deben implementarse estos componentes, sí establecen las
bases de nuestra implementación. Esta es una introducción de alto nivel a los componentes que
implementaremos a lo largo de este libro.

Action
Las actions son los verbos del sistema. De hecho, es útil si derivamos el nombre de un action
directamente de una oración. Estas frases son típicamente declaraciones de funcionalidad - algo que
queremos que la aplicación haga. He aquí algunos ejemplos:
• Traer la sesión
• Navegar a la página de configuración
• Filtrar la lista de usuarios
• Cambiar la visibilidad de la sección detalles.
Estas son capacidades simples de la aplicación, y cuando las implementamos como parte de una
arquitectura Flux, las acciones son el punto de partida. Estas declaraciones legibles por humanos a
menudo requieren otros componentes nuevos en otras partes del sistema, pero el primer paso es
siempre un action.
Entonces, ¿qué es exactamente un action de Flux? En su forma más simple, un action no es más que
un un nombre que ayuda a identificar el propósito de la acción. Más típicamente, las acciones
constan de un name y un payload. No te preocupes por los detalles de los payloads todavía, ya que
en lo que a actions se refiere, son sólo piezas opacas de datos que se entregan a el sistema. Dicho de
otra manera, las acciones son como entregas de corre. El punto de entrada a nuestro sistema Flux no
se preocupa por el contenido de la entrea, sólo que llegan a a donde tienen que ir. He aquí una
ilustración de las acciones que entran en un sistema Flux:

Este diagrama puede dar la impresión de que las actions son externas a Flux, cuando en realidad son
parte integral del sistema. La razón por la que esta perspectiva es valiosa es porque nos obliga a
pensar en las acciones como el único medio para entregar nuevos datos al
sistema.
Regla de oro de Flux: Si no es una acción, no puede suceder.

Dispatcher
El dispatcher en una arquitectura Flux es responsable de distribuir las acciones al componente store
(hablaremos del store a continuación). Un dispatcher es en realidad una especie de broker-si las
acciones quieren entregar nuevos datos a un storue, tienen que comunicarse con el broker, para que
pueda averiguar la mejor manera de entregarlos. Piensa en un broker de mensajes en un sistema
como RabbitMQ. Es el centro neurálgico donde todo se envía antes de que sea realmente entregado.
Aquí hay un diagrama que representa las acciones de recepción de un dispatcher de Flux y sus
envíos a los stores:

La sección anterior de este capítulo - "capas arquitectónicas simples"- no tenía una capa explícita
para los dispatchers. Eso fue intencional. En una aplicación Flux, sólo hay un despachador. Puede
considerarse más como una pseudocapa que como una capa explícita. Sabemos que el dispatcher
está ahí, pero no es un nivel de abstracción esencial. Lo que que nos preocupa a nivel arquitectónico
es asegurarnos de que dada una acción saber cuando va a llegar a todos los stores del sistema.
Dicho esto, el papel del dispatcher es crítico para el funcionamiento de Flux. Es el lugar donde se
registran las funciones de devolución de llamada y donde son manejadas las dependencias de datos.
Los stores le dicen al dispatcher acerca de otros stores de los que dependen, y el dispatcher se
asegura de que estas dependencias se manejan correctamente.
Regla de oro Flux: El dispatcher es el árbitro definitivo de las dependencias
de datos.

Store
Los stores son donde se mantiene el estado en una aplicación de Flux. Esto significa los datos de la
aplicación que son enviados al frontend desde la API. Sin embargo, los stores de Flux toman esto un
paso más allá y modelan explícitamente el estado de toda la aplicación. Si esto suena confuso o
como una mala idea en general, no te preocupes - aclararemos esto conforme nos abramos camino
en los capítulos subsiguientes. Por ahora, sólo quiero que sepas que los stores son donde se puede
encontrar el estado que importa. Otros componentes de Flux no tienen estado. tienen un estado
implícito a nivel de código, pero no estamos interesados en esto, desde el punto de vista
arquitectónico.
Las actions son el mecanismo de entrega de nuevos datos que entran en el sistema. El término
nuevo dato no implica que simplemente los agreguemos a alguna colección en una store. Todos los
datos que entran en el sistema son nuevos en el sentido de que no se han enviado, como una acción
- de hecho podría resultar desde un cambio de estado de un store. Echemos un vistazo a una
visualización de un action que resulta en un cambio de estado del store:

El aspecto clave de cómo los stores cambian de estado es que no hay una lógica externa que
determine que debe producirse un cambio de estado. Es el store, y sólo la store, el que toma esta
decisión y luego lleva a cabo la transformación del estado. Todo esto está bien encapsulado dentro
del store. Esto significa que cuando necesitamos razonar sobre una información en particular, no
necesitamos buscar más allá de los stores. Son sus propios jefes - son autónomos.
Regla de oro de Flux: Los stores son donde vive el estado, y sólo los stores
mismos pueden cambiar este estado.

Vista
El último componente de Flux que vamos a ver en esta sección es la vista, y ni siquiera es
técnicamente parte de Flux. Al mismo tiempo, las vista son obviamente un elemento crítico parte de
nuestra aplicación. Las vistas se entienden casi universalmente como la parte de nuestra aplicación
que es responsable de mostrar los datos al usuario: es la última parada de los flujos de datos a través
de nuestra arquitectura de información. Por ejemplo, en arquitecturas MVC, toman los datos del
modelo y los muestran. En este sentido, las vistas en una aplicación basada en Flux no son muy
diferentes de las vistas del MVC. En lo que difieren notablemente es en lo que respecta a manejar
eventos. Echemos un vistazo al siguiente diagrama:

Aquí podemos ver las responsabilidades contrastantes de una vista Flux, comparadas con un
componente de vista que se encuentra en la arquitectura típica de MVC. Las dos clases de vista
tienen tipos similares de datos que fluyen dentro de ellos - datos de aplicación usados para
renderizar las componentes y eventos (a menudo aportados por el usuario). Lo que difiere entre los
dos tipos de vista es lo que fluye desde ellos.
La vista típica no tiene realmente ninguna restricción en cómo su manejador de eventos se
comunican con otros componentes. Por ejemplo, en respuesta a un usuario haciendo clic en un
botón, la vista podría invocar directamente el comportamiento de un controlador, cambiar el
parámetro de un modelo, o puede consultar el estado de otra vista. Por otra parte, en la vista de Flux
sólo puede enviar nuevas acciones. Esto mantiene nuestro único punto de entrada en el sistema
intacto y coherente con otros mecanismos que quieren cambiar el estado de la los datos de nuestro
store. En otras palabras, una respuesta de API actualiza el estado exactamente de la misma manera
como lo hace un usuario al hacer clic en un botón.
Dado que las vistas deben limitarse en cuanto a la forma en que los datos salen de ellos (además de
las modificaciones DOM) en una arquitectura Flux, pensarás que las vistas deberían ser un
componente real de Flux. Esto tendría sentido en la medida en que hacer que las actions sean la
única opción de las vistas. Sin embargo, tampoco hay razón por la que no podamos hacer cumplir
esto, con la ventaja de que Flux permanece totalmente centrado en la creación de arquitecturas de la
información.
Ten presente, sin embargo, que Flux está todavía en su desarrollo. No hay duda de que va a haber
influencias externas a medida que más personas empiecen a adoptar Flux. Tal vez Flux tenga algo
que decir sobre las vistas en el futuro. Hasta entonces, las vistas existen fuera de Flux pero están
limitadas por la naturaleza unidireccional de Flux.
Regla de oro de Flux: La única manera de que los datos salgan de una vista
es enviando un action.

Instalando el paquete Flux


Cerraremos el primer capítulo mojándonos los pies con algo código, porque todo el mundo necesita
una aplicación "hola mundo". También quitaremos del camino algunas de nuestras tareas de
boilerplate de configuración de código, ya que usaremos una configuración similar a lo largo del
libro.
Nos saltaremos la instalación de Node + NPM ya que está suficiente cubierto
con gran detalle por todo Internet. Asumiremos que Node está instalado y
listo para avanzar.
El primer paquete NPM que necesitaremos instalar es Webpack. Se trata de un empaquetador de
módulos avanzado que se adapta perfectamente a las aplicaciones JavaScript modernas, incluidas
las aplicaciones basadas en Flux. Quisiéramos instalar este paquete globalmente para que el
comando webpack se instale en nuestro sistema:
npm install webpack -g
Con Webpack, podemos construir cada uno de los ejemplos de código de este libro. Sin embargo,
nuestro proyecto requiere un par de paquetes NPM locales, que pueden instalarse de la siguiente
manera:
npm install flux babel-core babel-loader babel-preset-es2015 –save-dev
La opción ­­save­dev añade estas dependencias de desarrollo a nuestro fichero, si existe. Esto
es sólo para empezar: no es necesario instalar manualmente estos paquetes para ejecutar los
ejemplos de código de este libro. Los ejemplos que ha descargado ya vienen con un
package.json, así que para instalar las dependencias locales, simplemente ejecute lo siguiente
desde el mismo directorio que el archivo package.json:
npm install
Ahora el comando webpack puede ser usado para construir el ejemplo. Este es el único ejemplo en
el primer capítulo, por lo que es fácil navegar a dentro de una ventana de terminal y ejecute el
comando webpack, que construye el archivo main­bundle.js. Alternativamente, si planeas
jugar con el código, lo cual es obviamente recomendable, intenta ejecutar webpack ­­watch .
Esta última forma del comando supervisará los cambios de archivo a los archivos usados en la
compilación, y ejecuta la compilación cada vez que cambien.
Este es de hecho un hola mundo simple para que nos pongamos en marcha, preparándonos para el
resto del libro. Nos hemos encargado de las tareas de configuración del boilerplate instalando
Webpack y sus módulos de soporte. Echemos un vistazo al código ahora. Empezaremos mirando el
lenguaje de marcas usado.
<!doctype html>
<html>
<head>
<title>Hello Flux</title>
<script src="main-bundle.js" defer></script>
</head>
<body></body>
</html>
No hay mucho que hacer, ¿verdad? Ni siquiera hay contenido dentro de la etiqueta body. La parte
importante es el script main­bundle.js - este es el código que ha sido construido para nosotros
por Webpack. Echemos un vistazo a este código ahora:
// Importa el módulo “flux”.
import * as flux from 'flux';
// Crea una nueva instancia de dispatcher. “Dispatcher es
// el único constructor útil que se encuentra el el módulo “flux”
const dispatcher = new flux.Dispatcher();
// Registra una funcion callback, invocada cada vez
// que un action es enviado.
dispatcher.register((e) => {
var p;
// Determina como responder a la action. En este caso,
// estamos simplemente creando un nuevo contenido usando
// la propiedad “payload”. La propiedad “type” determina como creamos
// el contenido.
switch (e.type) {
case 'hello':
p = document.createElement('p');
p.textContent = e.payload;
document.body.appendChild(p);
break;
case 'world':
p = document.createElement('p');
p.textContent = `${e.payload}!`;
p.style.fontWeight = 'bold';
document.body.appendChild(p);
break;
default:
break;
}
});
// Envía una action “hello”.
dispatcher.dispatch({
type: 'hello',
payload: 'Hello'
});
// Envía una action “world”.
dispatcher.dispatch({
type: 'world',
payload: 'World'
});
Como puedes ver, no hay mucho en esta aplicación Flux de hola mundo. De hecho, el único
componente específico de Flux que este código crea es dispatcher. A continuación, envía un par de
actions y la función del manejador que está registrada en el store procesa las acciones.
No te preocupes si no hay stores o vistas en este ejemplo. La idea es que tenemos el paquete básico
de Flux NPM instalado y listo para usar.

Sumario
Este capítulo te introdujo a Flux. Específicamente, observamos lo que es Flux y lo que no es. Flux
es un conjunto de patrones arquitectónicos que, cuando se aplican a nuestras Aplicaciones
JavaScript, ayuda para tener el flujo de datos correcto en nuestra arquitectura. Flux no es otro
framework utilizado para resolver implementaciones específicas, ya sean las peculiaridades del
navegador o beneficios en el rendimiento, hay una multitud de herramientas ya disponibles para
estos fines. Tal vez el aspecto definitorio más importante de Flux son los problemas conceptuales
que resuelve cosas como el flujo de datos unidireccional. Esta es una de las principales razones por
las que no existe una implementación de Flux de facto.
Terminamos el capítulo caminando a través de la configuración de nuestros componentes usados a
lo largo del libro. Para comprobar que los paquetes están todos en su sitio, hemos creado una
aplicación muy básica de hola mundo en Flux.
Ahora que tenemos una idea de lo que es Flux, es hora de que veamos por qué Flux es como es. En
el siguiente capítulo, echaremos un vistazo más detallado a los principios que impulsan el diseño de
las aplicaciones de Flux.
2
Principios de Flux
En el capítulo anterior, se presentó a un nivel muy alto algunos de los principios de Flux. Por
ejemplo, el flujo de datos unidireccional es central para la existencia de Flux. El objetivo de este
capítulo es ir más allá de la visión simplista de los principios de Flux.
Empezaremos con una retrospectiva del MVC para identificar dónde falla cuando estamos tratando
de escalar una arquitectura de frontend. Después de esto, tomaremos una mirada más profunda al
flujo de datos unidireccional y cómo resuelve algunas de los escalados que hemos identificado en
las arquitecturas MVC.
A continuación, abordaremos algunos problemas de composición de alto nivel a los que se
enfrentan las arquitecturas Flux, como hacer todo explícito y favorecer capas sobre jerarquías
profundas. Finalmente, compararemos los varios tipos de estado que se encuentran en una
arquitectura Flux e introduciremos el concepto de una ronda de actualización.

Desafíos con el MV*


MV* es el patrón arquitectónico predominante de las aplicaciones JavaScript de frontend. Nos
estamos refiriendo a esto como MV* porque hay un número de variaciones aceptadas en el que cada
uno de ellos tiene el modelo y la vista como conceptos centrales. Para nuestras discusiones en este
libro, todos pueden ser considerados el mismo estilo de arquitectura JavaScript.
MV* no se afianzó en la comunidad de desarrolladores porque es un conjunto terrible de patrones.
No, el MV* es popular porque funciona. Aunque se puede pensar en Flux como una especie de
sustituto de MV*, no hay necesidad de desarmar todas nuestras aplicaciones.
No existe una arquitectura perfecta, y Flux no es inmune a este hecho. El objetivo de esta sección
no es minimizar el MV* y todas las cosas que hace bien, sino más bien mirar algunas de las
debilidades del MV* y ver cómo Flux interviene y mejora la situación.

Separación de responsabilidades
Una cosa en la que MV* es realmente buena es en establecer una clara separación de
responsabilidades. Eso es que un componente tiene una responsabilidad, mientras que otro
componente es responsable de algo más, y así sucesivamente, en toda la arquitectura.
Complementario al principio de separación de responsabilidades es el principio de responsabilidad
única, que hace cumplir un una clara separación de responsabilidades.
Pero, ¿por qué nos importa? La respuesta simple es que cuando separamos las responsabilidades en
diferentes componentes, las diferentes partes del sistema se desacoplan de forma natural entre si.
Esto significa que podemos cambiar una cosa sin afectar necesariamente otra. Este es un rasgo
deseado de cualquier sistema de software, independientemente de la arquitectura. Pero, ¿es esto
realmente lo que obtenemos con el MV*, y es esto algo de lo que realmente nos tenemos que
preocupar?
Por ejemplo, tal vez no haya una ventaja clara al dividir una funcionalidad en cinco categorías
distintas responsabilidades. Tal vez el desacoplamiento del comportamiento de la funcionalidad en
realidad no logra nada porque tendríamos que tocar los cinco componentes cada vez que cambiemos
algo de todos modos. Así que en lugar de ayudarnos a crear una arquitectura robusta, el principio de
separación de preocupaciones no ha sido más que algo que indirectamente dificulta la
productividad. He aquí un ejemplo de una funcionalidad que está dividida en varias piezas con
responsabilidad única:

Cada vez que un desarrollador necesita aislar una funcionalidad para que pueda entender cómo
funciona, termina pasando más tiempo saltando entre los archivos de código fuente. La
funcionalidad está fragmentada, y no hay ninguna ventaja obvia en estructurar el código de esta
manera. Aquí está una vista piezas que componen una funcionalidad en una arquitectura Flux:

La descomposición de la funcionalidad en Flux nos deja una sensación de previsibilidad. Hemos


dejado fuera las formas potenciales en las que la vista en sí misma podría descomponerse, pero eso
se debe a que la las vistas están fuera de Flux. Todo lo que nos importa en términos de nuestra
arquitectura Flux es que la información correcta siempre se transmite a nuestras vistas cuando se
producen cambios de estado.
Observará que la lógica y el estado de una determinada funcionalidad en Flux están estrechamente
acopladas la una con la otra. Esto está en contraste con el MV*, donde queremos que la lógica de
aplicación sea una entidad independiente que pueda operar con cualquier dato. Lo contrario lo
encontramos en Flux, donde encontraremos la lógica responsable del cambo de estado muy cerca de
ese estado. Este es un rasgo de diseño intencional, con la implicación de que no tenemos la
necesidad de dejaros llevar por la separación de responsabilidades de unos y otros, y que esta
actividad a veces puede ser más inutil que ayudar.
Como veremos en los próximos capítulos, esta estrecha combinación de datos y lógica es
característica de los stores de Flux. El diagrama anterior muestra que con complejas es mucho más
fácil añadir más lógica y más estados, porque siempre están cerca de la superficie del elemento, en
lugar de enterrarlo en un árbol de componentes anidado.
Modificaciones en cascada
Es agradable cuando tenemos un componente de software que simplemente funciona. Esto podría
significar cualquier cantidad de cosas, pero su significado normalmente se centra en el manejo
automático de las cosas por nosotros. Por ejemplo, en lugar de tener que invocar manualmente este
método, seguido por ese método, y así sucesivamente, todo es manejado por el componente por
nosotros. Echemos un vistazo a la siguiente ilustración:

Cuando pasamos la información de entrada a un componente mayor, podemos esperar que haga lo
correcto automáticamente por nosotros. Lo que es convincente acerca de este tipo de componentes
es que significa menos código para nosotros. Después de todo, el componente sabe cómo
actualizarse orquestando la comunicación entre los subcomponentes.
Aquí es donde comienza el efecto de cascada. Le decimos a un componente que realice algún
comportamiento. Este, a su vez, hace que otro componente reaccione. Le damos una entrada que
hace que otro componente reaccione, y así sucesivamente. Pronto, es muy difícil comprender lo
que está pasando en nuestro código. Esto se debe a que las cosas de las que tenemos que ocuparnos
están ocultas a la vista. Intencionalidad por diseño, que implica consecuencias.
El diagrama anterior no es tan malo. Claro, puede ser un poco difícil de seguir dependiendo de
cuántos subcomponentes se añadan al componente más grande, pero en general, es un problema
tratable. Veamos una variación de este diagrama:

¿Qué es lo que acaba de pasar? Tres cajas más y cuatro líneas más acaban de ocurrir, resultando en
una explosión de complejidad de actualización en cascada. El problema ya no es tratable porque
simplemente no podemos manejar este tipo de complejidad, y la mayoría de las aplicaciones MV*
que dependen de este tipo de actualización automática tienen más de seis componentes. Lo mejor
que podemos esperar es que una vez que funcione como queremos, siga funcionando.
Esta es la ingenua suposición que hacemos sobre la actualización automática - es algo que
queremos encapsular. El problema es que esto generalmente no es cierto, al menos no si planeamos
mantener el software. Flux evita el problema de las actualizaciones en cascada porque sólo una
store puede cambiar su propio estado, y esto es siempre en respuesta a un action

Responsabilidad de actualización del modelo


En una arquitectura MV*, el estado se almacena dentro de los modelos. Para inicializar el estado del
modelo, podríamos obtener datos de la API de backend. Esto es bastante claro: creamos un nuevo
modelo, luego le decimos a ese modelo que vaya a buscar algunos datos. Sin embargo, el MV* no
dice nada sobre quién es responsable de actualizar estos modelos. Uno podría pensar que es el
componente controlador el que debería tener un control total sobre el modelo, ¿pero esto sucede
alguna vez en la práctica?.
Por ejemplo, ¿qué sucede en los manejadores de eventos de la, llamados en respuesta a la
interactividad del usuario? Si sólo permitimos que los controladores actualicen el estado de nuestros
modelos, entonces las funciones del manejador de eventos de la vista deben hablar directamente con
el controlador en cuestión. El siguiente diagrama es una visualización de un controlador que cambia
el estado de los modelos de diferentes maneras:

A primera vista, esta configuración del controlador tiene mucho sentido. Actúa como una envoltura
alrededor los modelos que almacenan el estado. Es una suposición segura que todo lo que quiera
cambiar en cualquiera de estos modelos necesita pasar por el controlador. Esa es su responsabilidad
después de todo para controlar las cosas. Datos que provienen de la API, eventos desencadenados
por el usuario y manejados por la vista, y otros modelos - todos estos necesitan hablar con el
controlador si quieren cambiar el estado de los modelos.
A medida que nuestro controlador crece, nos aseguramos de que los cambios en el estado del
modelo sean manejados por el controlador esto producirá más y más métodos que cambiarán el
estado del modelo. Si nosotros retrocedemos y miramos todos estos métodos a medida que se
acumulan, empezaremos a notar un montón de direccionamientos innecesarias. ¿Qué ganamos al
utilizar estos cambios de estado?
Otra razón por la que el controlador es un callejón sin salida para tratar de establecer un estado
consistente desde cambios en MV* son los cambios que los modelos pueden hacerse a sí mismos.
Por ejemplo, establecer una propiedad en un modelo podría terminar cambiando otras propiedades
del modelo como un efecto secundario. Peor aún, nuestros modelos podrían tener listeners que
respondan a los cambios de estado, en algún otro lugar del sistema (el problema de las
actualizaciones en cascada).
Los stores de Flux resuelven el problema de las actualizaciones en cascada permitiendo únicamente
cambios de estado a través de actions. Este mismo mecanismo resuelve los desafíos del MV* que se
discuten aquí; no tienen que preocuparse de que las vistas u otros stores cambien directamente el
estado de nuestro store.

Datos unidireccionales
Una piedra angular de cualquier arquitectura Flux es el flujo de datos unidireccional. La idea es que
los flujos de datos del punto A al punto B, o del punto A al B al C, o del punto A al C. Es la
dirección la que es importante con el flujo de datos unidireccional, y en menor medida el orden. Así
que cuando decimos que nuestra arquitectura usa una arquitectura unidireccional podemos decir que
los datos nunca fluyen del punto B al punto A. Se trata de una propiedad importante de las
arquitecturas Flux.
Como vimos en la sección anterior, las arquitecturas MV* no tienen una dirección discernible con
sus flujos de datos. En esta sección, hablaremos de algunas de las propiedades que hacen que valga
la pena implementar un flujo de datos unidireccional. Comenzaremos con un vistazo a los puntos de
partida y de finalización de nuestros flujos de datos, y luego pensaremos en cómo pueden evitarse
los efectos secundarios cuando los datos fluyen en una dirección.

De principio a fin
Si los datos fluyen en una sola dirección, tiene que haber un punto de partida y un punto de llegada.
En otras palabras, no podemos tener un flujo interminable de datos, que afecta arbitrariamente a los
diversos componentes por los que fluyen los datos. Cuando los flujos de datos son unidireccionales
con puntos de inicio y final claramente definidos, no hay forma de que podamos tener flujos
circulares. En cambio, tenemos un gran ciclo de flujo de datos en Flux, como se visualiza aquí:

Esto es obviamente una simplificación de cualquier arquitectura de Flux, pero sirve para ilustrar los
puntos de inicio y final de cualquier flujo de datos dado. Lo que estamos viendo es llamado a una
ronda de actualización. Una ronda es atómica en el sentido de que en su recorrido hasta su
completa ejecución, no hay manera de evitar que una ronda de actualización se complete (a menos
que una excepción sea lanzada).
JavaScript es un lenguaje de completa ejecución, lo que significa que una vez que se inicia la
ejecución de un bloque de código, va a terminar. Esto es bueno porque significa que una vez que
empezamos a actualizar la interfaz de usuario, no hay forma de que una función de devolución de
llamada pueda interrumpir nuestra actualización. La excepción es cuando nuestro propio código
interrumpe el proceso de actualización. Por ejemplo, nuestra la lógica de nuestro store pretende
cambiar el estado que el store envía a una action. Esto serían malas noticias para nuestra
arquitectura Flux porque violaría los datos unidireccionales de flujo. Para evitarlo, el dispatcher
puede detectar realmente cuándo tiene lugar una salida dentro de una ronda de actualización.
Tendremos más sobre esto en capítulos posteriores.
Las rondas de actualización son responsables de actualizar el estado de toda la aplicación, no sólo
las partes que se han suscrito a un tipo particular de action. Esto significa que a medida que nuestra
aplicación crece, también lo hacen nuestras rondas de actualizaciones. Ya que una ronda de
actualización toca cada store, puede empezar a sentirse como si los datos estuvieran fluyendo de
lado a lado a través de todas nuestras stores. He aquí una ilustración de la idea:

Desde la perspectiva del flujo de datos unidireccional, en realidad no importa cuántas stores haya.
Lo importante es recordar que las actualizaciones no se verán interrumpidas por otras acciones que
se estén enviando.

Sin efectos secundarios


Como vimos con las arquitecturas MV*, lo bueno de los cambios de estado automáticos es también
su desaparición. Cuando programamos por reglas ocultas, estamos programando esencialmente
uniendo un montón de efectos secundarios. Esto no escala bien, principalmente debido al hecho de
que es imposible mantener todas estas conexiones ocultas en nuestra cabeza en un momento dado. A
Flux le gusta evitar los efectos secundarios siempre que sea posible.
Pensemos por un momento en las stores. Estos son los árbitros de estado en nuestra aplicación.
Cuando algo cambia de estado, tiene el potencial de causar que otro código se ejecute como
respuesta. Esto sí sucede en Flux. Cuando una store cambia de estado, las vistas pueden ser
notificadas sobre el cambio, si se han suscrito a la directiva de la store. Este es el único lugar donde
los efectos secundarios ocurren en Flux, lo cual es inevitable, ya que necesitamos actualizar el
DOM en algún momento cuando cambie el estado. Pero lo que es diferente acerca de Flux es cómo
evita los efectos secundarios cuando hay dependencias de datos involucrados. El enfoque típico
para tratar las dependencias de datos en las interfaces de usuario es notificar al modelo dependiente
que algo ha pasado. Piensa en una cascada actualizaciones, como se ilustra aquí:

Cuando hay una dependencia entre dos stores en Flux, sólo necesitamos declarar esta dependencia
en la store dependiente. Lo que esto hace es decirle al dispatcher que se asegure de que la store de la
que dependemos esté siempre actualizada primero. Entonces, la store dependiente puede utilizar
directamente los datos de store de los que depende. De este modo, todas las actualizaciones pueden
tener lugar en la misma ronda de actualización.

Explícito sobre implícito


Con los patrones arquitectónicos, la tendencia es hacer las cosas más fáciles velándolas detrás de
abstracciones que se vuelven más elaboradas con el tiempo. Eventualmente, más y más de los
cambios automáticos de datos del sistema y la conveniencia del desarrollador es reemplazada por la
complejidad oculta.
Este es un problema real de escalabilidad, y Flux lo maneja favoreciendo acciones explícitas y
transformaciones de datos sobre abstracciones implícitas. En esta sección, exploraremos el
beneficio de lo explícito junto con las concesiones que deben hacerse.

Modificaciones mediante efectos secundarios ocultos


Ya hemos visto, en este capítulo, lo difícil que puede ser tratar con los cambios de estado que se
esconden detrás de las abstracciones. Nos ayudan a evitar escribir código, pero dificulta la
comprensión de todo un flujo de trabajo cuando llegamos y miramos el código más tarde. Con Flux,
el estado se mantiene en un store, y el store es responsable de cambiar su propio estado. Lo bueno
de esto es que cuando queremos preguntar cómo cambia de estado una determinada store, todo el
código de transformación de estado está ahí, en un solo lugar. Veamos un ejemplo de store:
// Un store Flux con estado.
class Store {
constructor() {
// El estado inicial del store.
this.state = { clickable: false };
// Toda la transformación del estado ocurre
// aquí. La propiedad “action.type” es la que
// determina que cambios tendrán lugar.
dispatcher.register((e) => {
// Dependiendo del tipo de action,
// usaremos "Object.assign()" para asignar diferentes
// valores a "this.state".
switch (e.type) {
case 'show':
Object.assign(this.state, e.payload,
{ clickable: true });
break;
case 'hide':
Object.assign(this.state, e.payload,
{ clickable: false });
break;
default:
break;
}
});
}
}
// Crea una nueva instancia de store.
var store = new Store();
// Envío una action “show”.
dispatcher.dispatch({
type: 'show',
payload: { display: 'block' }
});
console.log('Showing', store.state);
// → Showing {clickable: true, display: "block"}
// Envío de una action “hide”.
dispatcher.dispatch({
type: 'hide',
payload: { display: 'none' }
});
console.log('Hiding', store.state);
// → Hiding {clickable: false, display: "none"}
Aquí tenemos una store con un simple objeto de estado. En el constructor, la tienda registra una
función de devolución de llamada con el dispatcher . Todas las transformaciones de estado
tienen lugar, explícitamente, en una función. Aquí es donde los datos se convierten en información
para nuestra interfaz de usuario. No tenemos que buscar los pequeños bits y piezas de datos a
medida que cambian de estado a través de múltiples componentes; esto no sucede en Flux.
Así que la pregunta ahora es, ¿cómo hacen las vistas uso de estos datos de estado monolíticos? En
otros tipos de arquitectura de frontend, las vistas se notifican cada vez que hay cualquier cambio de
estado. En el ejemplo anterior, una vista se notifica cuando la propiedad clickable cambia y de
nuevo cuando la propiedad display cambia. La vista tiene lógica para que estos dos cambios sean
independientes el uno del otro. Sin embargo, las vistas en Flux no consiguen actualizaciones
refinadas como estas. En lugar de eso, se les notifica cuando el estado de la store cambia y los datos
del estado es lo que se les da.
La implicación aquí es que debemos inclinarnos hacia una tecnología que sea buena, al volver a
renderizar componentes enteros. Esto es lo que hace que React se adapte bien a las arquitecturas
Flux. No obstante, somos libres de usar cualquier tecnología frontend que queramos, como veremos
más adelante en el libro.

Los datos cambian de estado en un solo lugar


Como vimos en la sección anterior, el código de transformación de la store está encapsulado dentro
de la store. Esto es intencional. Se supone que el estado está cerca de el código de transformación
que cambia una store. La proximidad reduce drásticamente la complejidad de averiguar dónde
ocurren los cambios de estado a medida que los sistemas se vuelven más complejos. Este hace que
los cambios de estado sean explícitos, en lugar de abstractos e implícitos.
Una compensación potencial con tener una store que administre toda la transformación del estado es
que podría haber muchas. El código que miramos usaba un solo switch para manejar toda la
lógica de transformación de estado. Esto obviamente causaría un poco de dolor de cabeza más
tarde, cuando hay muchos casos que tratar. Pensaremos sobre esto más adelante en el libro, cuando
llegue el momento de considerar grandes y complejas stores. Sólo tienes que saber que podemos
refactorizar nuestras tiendas para manejar con elegancia un gran número a la vez que se mantiene el
acoplamiento entre la lógica de negocio y el estado.
Esto nos lleva de nuevo al principio de separación de responsabilidades. Con los stores Flux, los
datos y la lógica que opera sobre ellos no están separados en absoluto. ¿Esto es algo malo? Se envía
un action, se notifica a una store y ésta cambia de estado, (o no hace nada, ignorando la acción). La
lógica que cambia el estado se encuentra en el mismo componente porque no hay nada que ganar
moviéndolo a otra parte.

¿Demasiadas actions?
Las actions hacen explícito todo lo que sucede en una arquitectura Flux. Con todo me refiero a todo:
si sucede, es el resultado de una action enviada. Esto es bueno porque es fácil saber desde dónde se
envían las actions. Incluso a medida que el sistema crece, los envíos de action son fáciles de
encontrar en nuestro código, porque sólo pueden venir de un puñado de lugares. Por ejemplo, no
encontraremos acciones que se envíen desde las stores.
Cualquier funcionalidad que creemos tiene el potencial de crear docenas de actions, si no más.
Tendemos a pensar que más significa malo, desde una perspectiva arquitectónica. Si hay más de
algo, va a ser más difícil de escalar y de programar. Hay algo de verdad en esto, pero si vamos a
tener mucho de algo, que es inevitable en cualquier sistema grande, es bueno que sean actions. Las
acciones son relativamente ligeras en el sentido de que describen algo que sucede en nuestra
aplicación. En pocas palabras, las actions no son cosas de peso que nos preocupen por tener
muchas.
¿Tener un montón de actions significa que tenemos que meterlas todas en un enorme módulo de
actions monolítico? Afortunadamente, no tenemos que hacer esto. Sólo porque las actions son el
punto de entrada a cualquier sistema Flux, no significa que no podamos modularizarlas a nuestro
gusto. Esto es cierto en todos los componentes de Flux que desarrollamos, y vamos a mantener un
ojo abierto para que podamos mantener nuestro código modular a medida que avanzamos en el
libro.

Capas sobre jerarquías


Las interfaces de usuario son de naturaleza jerárquica, en parte porque HTML es intrínsecamente
jerárquico y en parte por la forma en que estructuramos la información a los usuarios. Por ejemplo,
esta es la razón por la que hemos anidado los niveles de navegación en algunas aplicaciones: no es
posible que quepan todos los elementos de la pantalla a la vez. Naturalmente, nuestro código
comienza a reflejar esta estructura jerárquica convirtiéndose en una jerarquía en sí misma. Esto es
bueno en el sentido de que refleja lo que el usuario ve. Es malo en el sentido de que las jerarquías
profundas son difíciles de comprender.
En esta sección, veremos las estructuras jerárquicas en arquitecturas frontend y cómo Flux es capaz
de evitar jerarquías complejas. Primero cubriremos la idea de tener varios componentes de nivel
superior, cada uno con sus propias jerarquías. Entonces, miraremos los efectos secundarios que
ocurren dentro de las jerarquías y cómo fluyen los datos a través de las capas de flujo.

Múltiples jerarquías de componentes


Una aplicación dada, probablemente, tiene un puñado de principales. Estas son a menudo
implementadas como componentes o módulos de alto nivel en nuestro código. Estos no son
componentes monolíticos; se descomponen en componentes cada vez más pequeños. Quizás
algunos de estos componentes comparten componentes multipropósito más pequeños. Por ejemplo,
una jerarquía de componentes de nivel superior podría estar compuesta de modelos, vistas y
controladores, como se ilustra aquí:
Esto tiene sentido en términos de estructura de nuestra aplicación. Cuando miramos a las jerarquías
de componentes, es fácil ver de qué está hecha nuestra aplicación. Cada una de estas jerarquías, con
el componente de nivel superior como raíz, son como un poco universos que existen independientes
unos de otros. De nuevo, volvemos a la noción de separación de responsabilidades. Podemos
desarrollar una funcionalidad sin afectar a otra.
El problema con este enfoque es que las de la interfaz de usuario a menudo dependen de otras . En
otras palabras, el estado de una jerarquía de componentes dependerá probablemente de el estado de
otra. ¿Cómo mantenemos estos dos árboles de componentes sincronizados cuando no hay un
mecanismo para controlar cuando el estado puede cambiar? Lo que acaba ocurriendo es que un
componente en una jerarquía introducirá una dependencia arbitraria de un componente de otra
jerarquía. Esto sirve para una sola por lo que tenemos que seguir introduciendo nuevas
dependencias interjerárquicas para asegurarnos de que todo está sincronizado.

Profundidad de jerarquía y efectos secundarios


Un desafío con las jerarquías es la profundidad. Es decir, ¿hasta dónde se extenderá una jerarquía
determinada? Las de nuestra aplicación están en constante cambio y expansión. Esto puede llevar a
que nuestros árboles de componentes crezcan más altos. Pero también se ensanchan. Por ejemplo,
digamos que nuestra funcionalidad utiliza una jerarquía de componentes que tiene tres niveles de
profundidad.
Luego, añadimos un nuevo nivel. Bueno, probablemente tendremos que añadir varios componentes
nuevos a este nuevo nivel y en niveles superiores. Así que para construir sobre nuestras jerarquías,
tenemos que escalar en múltiples direcciones, horizontal y verticalmente. Esta idea se ilustra aquí:

Es difícil escalar componentes en múltiples direcciones, especialmente en jerarquías de


componentes donde no hay dirección de flujo de datos. Es decir, la entrada que termina cambiando
el estado de algo puede entrar en la jerarquía a cualquier nivel. Sin duda, esto tiene algún tipo de
efecto secundario, y si dependemos de componentes de otras jerarquías, se pierde toda esperanza.
Flujo de datos y capas
Flux tiene capas arquitectónicas distintas, porque son más favorables para escalar las arquitecturas
que las jerarquías. La razón es simple: sólo necesitamos escalar componentes horizontalmente,
dentro de cada capa de arquitectura. No necesitamos añadir nuevos componentes a una capa y
añadir nuevas capas. Echemos un vistazo a cómo es la escalabilidad de una arquitectura Flux en el
siguiente diagrama:

No importa cuán grande sea una aplicación, no hay necesidad de añadir nuevas capas
arquitectónicas. Simplemente añadimos nuevos componentes a estas capas. La razón por la que
podemos hacer esto sin crear un enredo de conexiones de componentes dentro de una capa dada es
porque las tres capas juegan un papel en la ronda de actualización. Una ronda de actualización
comienza con una acción y se completa con la última vista que se muestra. Los datos fluyen a través
de nuestra aplicación de capa a capa, en una dirección.

Datos de aplicación y estado UI


Cuando tenemos una separación de responsabilidades que tenga la vista en un lugar y los datos de
aplicación en otro, tenemos dos lugares distintos donde necesitamos gestionar el estado. Sin
embargo en Flux, el único lugar donde hay estado es dentro de una store. En esta sección,
compararemos los datos de la aplicación y los datos de la interfaz de usuario. A continuación,
abordaremos las transformaciones que, en última instancia, conducen a cambios en la interfaz de
usuario. Por último, discutiremos la naturaleza centrada en las de las stores Flux.

Más de lo mismo
Muy a menudo, los datos de la aplicación que se obtienen de una API se introducen en algún tipo de
capa de visualización. Esto también se conoce como la capa de presentación, responsable de
transformar los datos de la aplicación en algo de valor para el usuario - de datos a información, en
otras palabras. En estas capas, terminamos con el estado para representar los elementos de la
interfaz de usuario. Por ejemplo, ¿está marcada la casilla de selección? He aquí una ilustración de
cómo tendemos a agrupar los dos tipos de estado dentro de nuestros componentes:
Esto no encaja muy bien con las arquitecturas Flux, porque las stores están donde pertenece el
estado, incluyendo la interfaz de usuario. Entonces, ¿puede una store tener tanto el estado de la
aplicación como el de la interfaz de usuario dentro de ella? Bueno, no hay un argumento fuerte en
contra. Si todo lo que tiene un estado es autocontenido dentro de un store, debería ser bastante
simple discernir entre los datos de la aplicación y el estado que pertenece a los elementos de la
interfaz de usuario. He aquí una ilustración de los tipos de estado que se encuentran en las stores
Flux:

La idea errónea fundamental al tratar de separar el estado de UI de otro estado es que los
componentes a menudo dependen del estado de la interfaz de usuario. Incluso componentes de
interfaz de usuario de diferentes pueden depender del estado de otro de maneras impredecibles.
Flux reconoce esto y no trata de tratar el estado de UI como algo especial que debe ser separado de
los datos de aplicación.
El estado de interfaz de usuario que termina en una store puede derivarse de varias cosas.
Generalmente, dos o más elementos de nuestros datos de aplicación podrían determinar un estado
de un componente UI. Un estado UI podría derivarse de otro estado UI, o de algo más complejo,
como un estado de UI y otros datos de aplicación. En otros casos, los datos de aplicación son tan
simples que pueden ser consumidos directamente por la vista. La clave es que la vista tiene
suficiente información que puede rendir por sí misma sin tener que rastrear su propio estado.

Transformaciones fuertemente acopladas


Los datos de aplicación y el estado de la interfaz de usuario están fuertemente acoplados en los
stores Flux. Sólo tiene sentido que las transformaciones que operan sobre estos datos estén
estrechamente vinculadas a la store. Esto hace que sea fácil para nosotros cambiar el estado de la
interfaz de usuario basada en otros datos de aplicación o en función del estado de otras stores.
Si nuestro código de lógica de negocio no estuviera en la store, entonces tendríamos que empezar a
introducir dependencias de los componentes que contienen la lógica que necesita la store. Claro,
esto significaría una lógica de negocio genérica que transforme el estado, y esto podría ser
compartido en varias stores, pero esto rara vez ocurre a un alto nivel. Las stores son mejores de
mantener si su lógica de negocio que transforma el estado de la store está fuertemente acoplado. Si
necesitamos reducir código repetitivo, podemos introducir códigos más pequeños y de funciones de
utilidad para ayudar con las transformaciones de datos.
Podemos ser genéricos con nuestras stores también. Estas stores son
abstractas y no interactúan directamente con las vistas. Profundizaremos en
este tema avanzado más adelante en el libro.

Centrado en las funcionalidades


Si las transformaciones de datos que cambian el estado de un store están fuertemente acopladas
¿significa esto que la store está hecha a medida para una funcionalidad específica? En otras
palabras, ¿nos preocupan las stores que se reutilizan para otras funciones? Claro, en algunos casos
tenemos datos genéricos que no tienen mucho sentido si se repiten varias veces a través de stores.
Pero en términos generales, las stores son específicas. Las son sinónimas con dominios en lenguaje
Flux - cada uno divide las capacidades de su interfaz de usuario de diferentes maneras.
Esto se diferencia de otras arquitecturas que basan sus modelos de datos en los datos de la API.
Luego, utilizan estos modelos para crear modelos de vista más específicos. Cualquier framework
MV* tendrá un montón de en sus abstracciones de modelos, cosas como los enlaces de datos y la
obtención automática de la API. Sólo están preocupados por almacenar estados y publicar
notificaciones cuando este estado cambia.
Cuando las stores nos animan a crear y almacenar nuevos estados específicos de la interfaz de
usuario, podemos diseñar más fácilmente para el usuario. Esta es la diferencia fundamental entre
Flux y modelos en otras arquitecturas: el modelo de datos de la interfaz de usuario es lo primero.
Las transformaciones dentro de las stores existen para asegurar que se publica el estado correcto -
todo lo demás es secundario.

Sumario
Este capítulo le introdujo a los principios de Flux. Estos deben estar presentes en su mente mientras
trabaja en cualquier arquitectura Flux. Comenzamos el capítulo con una breve retrospectiva de las
arquitecturas de estilo MV* que son mayoritarias en el desarrollo frontend. Algunos de los desafíos
de este estilo de arquitectura son las modificaciones en cascada del model y la falta de dirección del
flujo de datos. Luego miramos el concepto de flujo de datos unidireccional de Flux.
A continuación, cubrimos cómo Flux favorece las acciones explícitas sobre las abstracciones
implícitas. Esto hace las cosas más fáciles de comprender al leer el código de Flux, porque no
tenemos que buscar la causa de un cambio de estado. También vió cómo Flux utiliza capas
arquitectónicas para visualizar cómo fluyen los datos en una dirección a través del sistema.
Finalmente, comparamos los datos de la aplicación con el estado que generalmente se considera
específico de los elementos de la interfaz de usuario. Los stores de Flux tienden a centrarse en el
estado que es relevante para las , y no distingue entre los datos de la aplicación y el estado de la
interfaz de usuario. Ahora que tenemos una idea de los principios de las arquitecturas de Flux, es
hora de que codifiquemos. En el próximo capítulo, implementaremos nuestra arquitectura esqueleto
Flux, lo que nos permitirá centrarnos en el diseño de la información.
3
Construyendo una arquitectura esqueleto
La mejor manera de pensar en Flux es escribir código en Flux. Esta es la razón por la que queremos
empezar construir una arquitectura esqueleto tan pronto como sea posible. A esta fase de la
construcción en nuestra aplicación la llamamos arquitectura esqueleto porque aún no es la
arquitectura completa. Faltan muchos componentes clave de la aplicación, y esto es a propósito. El
objetivo del esqueleto es mantener las partes móviles a un mínimo, lo que nos permite centrarnos en
la información que nuestras stores generarán para nuestras vistas.
Despegaremos con una estructura minimalista que, aunque pequeña, no requiere mucho trabajo para
convertir nuestra arquitectura esquelética en nuestro código base. Entonces, nos moveremos a
algunos de los objetivos de diseño de la información de la arquitectura esqueleto. Lo siguiente que
haremos será la implementación de algunos aspectos de nuestras stores.
A medida que comencemos a construir, empezaremos a tener una idea de cómo se relacionan estas
tiendas con los dominios-las con las que nuestros usuarios interactuarán. Después de esto, vamos a
crear algunas vistas realmente simples que pueden ayudarnos a asegurarnos de que nuestros flujos
de datos están llegando a su destino final. Finalmente, terminaremos el capítulo repasando una lista
de verificación para cada uno las capas arquitectónicas de Flux, para asegurarnos de que hemos
validado nuestro esqueleto antes pasar a otras actividades de desarrollo.

Organización general
Como primer paso en la construcción de una arquitectura esqueleto Flux, pasaremos unos minutos
organizándonos. En esta sección, estableceremos una estructura básica de directorios,
averiguaremos cómo gestionaremos nuestras dependencias y elegiremos nuestras herramientas de
compilación. Nada de esto está escrito en piedra - la idea es ponerse en marcha rápidamente, pero al
mismo tiempo, establecer algunas normas para que la transformación de nuestra arquitectura
esqueleto en código de aplicación sea lo más fluida posible.

Estructura de directorios
La estructura de directorios utilizada para empezar a construir nuestro esqueleto no tiene por qué ser
elegante. Es una arquitectura esqueleto, no la arquitectura completa, por lo que la estructura de
directorios inicial debería seguir su ejemplo. Dicho esto, tampoco queremos utilizar una estructura
de directorios que sea difícil de evolucionar hacia lo que realmente se utiliza en el producto.
Echemos un vistazo a los elementos que encontraremos en la raíz de nuestro directorio de
proyectos:
Bastante simple, ¿verdad? Vamos a repasar lo que representa cada uno de estos elementos:
• main.js: Este es el principal punt de entrada a la aplicación. Este módulo JavaScript
inciará las actions iniciales dels sistema.
• dispatcher.js: Este es nuestro módulo disptacher. Aquí es donde se crea la instancia
del dispatcher de Flux.
• actions: Este directorio contiene todas nuestras funciones creadoras de actions y nuestras
constantes actions.
• stores: Este directorio contiene nuestros módulos stores.
• views: Este directorio contiene nuestros módulos de vistas.
Esto puede no parecer mucho, y esto es por diseño. La disposición del directorio refleja las capas
arquitectónicas de Flux. Obviamente habrá más en una aplicación real una vez que pasemos la fase
de arquitectura esqueleto, pero no mucho. Sin embargo, deberíamos abstenernos de añadir
componentes extraños en este punto, porque la arquitectura esqueleto es sobre todo el diseño de la
información.

Gestión de dependencias
Como punto de partida, vamos a necesitar el dispatcher básico de Flux de Facebook como una
dependencia de nuestra arquitectura esquelo - incluso si no terminamos usando esto en nuestro
producto final. Tenemos que empezar a diseñar nuestras stores, ya que esto es el aspecto más crucial
y que consume más tiempo de la arquitectura esqueleto; preocuparse por cosas como el dispatcher
en este momento simplemente no vale la pena.
Tenemos que empezar por algún sitio y la implementación del dispatcher de Facebook es
suficientemente buena. La pregunta es, ¿necesitaremos otros paquetes? En el Capítulo 1, ¿Qué es el
Flux? caminamos a través de la configuración del paquete Flux NPM de Facebook y usamos
Webpack para construir nuestro código. ¿Puede esto funcionar como nuestro eventual sistema de
producción?
No tener un gestor de paquetes o un empaquetador de módulos nos pone en desventaja, desde el
inicio del proyecto. Por eso tenemos que pensar en la dependencia como un primer paso de la
arquitectura esqueleto, a pesar de que no tenemos muchas dependencias en este momento. Si esta es
la primera vez que estamos construyendo un proyecto que tiene una arquitectura Flux detrás, la
forma en que manejamos las dependencias servirá como un plan futuro para proyectos posteriores
de Flux.
¿Es una mala idea añadir más dependencias de módulos durante el desarrollo de nuestro
arquitectura esqueleto? No, en absoluto. De hecho, es mejor que usemos una herramienta que sea
adecuada para el trabajo. Mientras implementamos el esqueleto, empezaremos a ver lugares en
nuestras stores donde una biblioteca sería de ayuda. Por ejemplo, si estamos clasificando y filtrando
un montón de colecciones de datos y estamos construyendo funciones de orden superior, usar algo
como lodash para esto es perfecto.
Por otro lado, tirar de algo como ReactJS o jQuery en esta fase no tienen mucho sentido porque
todavía estamos pensando en la información y no cómo presentarlo en el DOM. Así que ese es el
enfoque que vamos a usar en este libro-NPM como nuestro gestor de paquetes y Webpack como
nuestro empaquetador. Esta es la infraestructura base que necesitamos, sin mucha sobrecarga que
nos distraiga.

Diseño de la información
Sabemos que la arquitectura esqueleto que estamos tratando de construir está específicamente
enfocada en poner la información correcta en manos de nuestros usuarios. Esto significa que no
estamos prestando mucha atención a la interactividad del usuario o a dar formato a la información
en un archivo de fácil manejo. Podría ayudarnos si nos fijamos algunas metas aproximadas - ¿cómo
sabremos que realmente estamos llegando a algo con nuestro diseño de información?
En esta sección, hablaremos de la influencia negativa que los modelos de datos de API pueden tener
en nuestro diseño de interfaz de usuario. Luego, miraremos el mapeo de datos que el usuario ve y
cómo estos mapeos deben ser fomentados en nuestras stores. Finalmente, pensaremos en el entorno
en el que trabajamos.

Los usuarios no entienden los modelos


Nuestro trabajo como programadores de interfaz de usuario consiste en proporcionar la información
correcta al usuario en el momento adecuado. ¿Cómo lo hacemos? La sabiduría convencional gira en
torno a tomar algunos datos que obtuvimos de la API y luego renderizarlos como HTML. Aparte del
lenguaje de marcas y algunos estilos, nada ha cambiado mucho con los datos desde que llegaron de
la API. Estamos diciendo que aquí están los datos que tenemos, vamos a hacer que se vea bien para
el usuario. He aquí una ilustración de esta idea:

No se está produciendo ninguna transformación de datos aquí, lo que está bien, siempre y cuando el
usuario esté consiguiendo lo que necesita. El problema que este cuadro presenta es que el modelo
de datos de la API ha tomado como rehén el desarrollo de de interfaz de usuario. Debemos prestar
atención a todo lo que es enviado a nosotros desde el backend. La razón por la que esto es un
problema es porque estamos limitados en lo que realmente podemos hacer por el usuario. Algo que
podemos hacer es mejorar los datos que se envían desde la API en nuestros propios modelos. Esto
significa que si estamos trabajando en una funcionalidad que requeriría información que no es
exactamente como la API, podemos fabricarlo como un modelo de frontend, como se muestra aquí:
Esto nos acerca un poco más a nuestro objetivo en el sentido de que podemos crear un modelo de la
funcionalidad que estamos tratando de implementar y ponerlo delante del usuario. Por lo tanto,
aunque la API podría no ofrecer exactamente lo que queremos mostrar en pantalla, podemos utilizar
nuestras funciones de transformación para generar un modelo de la información que necesitamos.
Durante la fase de arquitectura esqueleto de nuestro proceso de diseño, debemos pensar en stores
independientes de las API's tanto como sea posible. No completamente independiente; no
queremos meternos en un jardín, poniendo en peligro el producto. Pero la idea de de producir una
arquitectura esqueleto Flux es asegurarnos de que estamos produciendo la información, en primer
lugar. Si no hay forma de que la API pueda soportar lo que estamos intentando hacer, entonces
podemos tomar las medidas necesarias, antes de pasar mucho tiempo implementando completas.

Stores mapean de lo que ve el usuario


El estado no es lo único que está encapsulado por las stores que se encuentran en nuestra
arquitectura Flux. También están las transformaciones de datos que asignan a un estado antiguo un
nuevo estado. Deberíamos pasar más tiempo pensando en lo que el usuario necesita ver y menos
tiempo pensando en los datos de la API, lo que significa que las funciones de transformación de la
store son esenciales.
Necesitamos adoptar las transformaciones de datos en los stores Flux, porque son los últimos
determinantes de cómo cambian las cosas ante de los ojos del usuario. Sin estos el usuario sólo
podrá ver información estática. Por supuesto, podríamos apuntar a diseñar una arquitectura que sólo
utilice los datos que se pasan al sistema de base de datos "tal cual", sin transformarlos. Esto nunca
funciona como pretendemos, por la simple razón de que vamos a descubrir dependencias con otros
componentes de la interfaz de usuario.
¿Cuáles deberían ser nuestros primeros objetivos con las stores y cómo transformamos su estado?
Bueno, la arquitectura esqueleto se basa en la experimentación, y si empezamos a escribir las
transformaciones, es probable que descubramos las dependencias antes. Las dependencias no son
necesariamente algo malo, excepto cuando encontramos muchas de ellas más tarde en el proyecto,
mucho después de que hayamos completado la fase de arquitectura esqueleto. Por supuesto, nuevas
van a añadir nuevas dependencias. Si podemos usar las transformaciones de estado para identificar
posibles dependencias, entonces podremos evitar futuros dolores de cabeza.

¿Con qué tenemos que trabajar?


Lo último que tendremos que considerar antes de arremangarnos y empezar a implementar esta
arquitectura esqueleto de Flux es lo que ya está en su lugar. Por ejemplo, ¿esta aplicación ya tiene
una API establecida y estamos rediseñando la interfaz? ¿Necesitamos conservar la experiencia de
usuario de una interfaz de usuario existente? ¿Es el proyecto completamente nuevo, sin entrada de
API o experiencia de usuario?
El siguiente diagrama ilustra cómo estos factores externos influyen en la forma en que tratamos la
implementación de nuestra arquitectura esqueleto:
No hay nada malo en que estos dos factores den forma a nuestra arquitectura Flux. En en el caso de
las APIs existentes, tendremos un punto de partida desde el que podremos empezar a escribir
nuestras funciones de transformación de estados, para obtener la información que el usuario
necesita. En el caso de mantener una experiencia de usuario existente, ya sabemos el objetivo de
nuestra información, y podemos trabajar las funciones de transformación desde un ángulo diferente.
Cuando la arquitectura Flux es completamente nueva, podemos dejar que informe tanto la
experiencia de usuario como las APIs que deben implementarse. Es muy poco probable que
cualquiera de los escenarios en los que nos encontremos construyendo una arquitectura esqueleto
sea tan cerrada. Estos son sólo los puntos de partida en los que podemos encontrarnos. Dicho esto,
es hora de empezar a implementar algunas stores en el esqueleto.

Puesta en marcha de las stores


En esta sección, vamos a implementar algunas stores en nuestra arquitectura esqueleto. No serán
stores completos capaces de soportar flujos de trabajo de principio a fin. Sin embargo, vamos a ver
dónde encajan las stores dentro del contexto de nuestra aplicación.
Empezaremos con la más básica de todas las acciones de la store, que serán pobladas con algunos
datos; esto se hace normalmente obteniéndolos a través de alguna API. Entonces, discutiremos el
cambio de estado de los datos remotos de la API. Finalmente, analizaremos las acciones que
cambian el estado de una store localmente, sin el uso de una API.

Obtención de datos de API


Independientemente de si existe o no una API con datos listos para consumir, sabemos que, con el
tiempo, así es como rellenaremos los datos de nuestras stores. Así que tiene sentido que pensemos
en esto como la primera actividad de diseño de la implementación de store en el esqueleto.
Vamos a crear una store básica para la página de inicio de nuestra aplicación. La información obvia
que el usuario va a querer ver aquí es el usuario actualmente conectado, un menú de navegación, y
quizás una lista resumida de eventos recientes que son relevantes para el usuario. Esto significa que
obtener estos datos es una de las primeras cosas que nuestra aplicación tendrá que hacer. Aquí está
nuestra primera implementación de la store:
// Importa el dispatcher, así la store puede
// escuchar el envío de eventos.
import dispatcher from '../dispatcher';
// Nuestro store “Home”.
class HomeStore {
constructor() {
// Inicializa un estado por defecto para nuestra store. Esto
// nunca es una mala idea, en caso de que otros stores quieran
// iterar sobre un conjunto de valores – se interrumpirá si
// son undefined.
this.state = {
user: '',
events: [],
navigation: []
};
// Cuando un evento “HOME_LOAD” sea enviado, podremos
// asignar el “payload” a “state”.
dispatcher.register((e) => {
switch (e.type) {
case 'HOME_LOAD':
Object.assign(this.state, e.payload);
break;
}
});
}
}
export default new HomeStore();
Esto es bastante fácil de seguir, así que vamos a señalar lo importante. Primero, necesitamos
importar el dispatcher para poder registrar nuestro store. Cuando se crea la store, el estado
predeterminado se almacena en la propiedad state. Cuando se envía la acción HOME_LOAD,
cambiamos el estado de la store. Por último, exportamos la instancia de la tienda como miembro
predeterminado del módulo.
Como indica el nombre de la action, HOME_LOAD se envía cuando se han cargado los datos de la
store. Presumiblemente, vamos a extraer estos datos para la store en home desde algunos endpoints
de la API. Sigamos, pongamos esta store en uso en nuestro módulo main.js - nuestro punto de
entrada a la aplicación:
// Importa el "dispatcher", y “homeStore”.
import dispatcher from './dispatcher';
import homeStore from './stores/home';
// Carga el estado por defecto de la store, antes
// de que cualquier action sea disparada contra ella.
console.log(`user: "${homeStore.state.user}"`);
// → user: ""
console.log('events:', homeStore.state.events);
// → events: []
console.log('navigation:', homeStore.state.navigation);
// → navigation: []
// Envía un evento, cuando puebla la
// "homeStore" con datos en el "payload" del evento.
dispatcher.dispatch({
type: 'HOME_LOAD',
payload: {
user: 'Flux',
events: [
'Completed chapter 1',
'Completed chapter 2'
],
navigation: [
'Home',
'Settings',
'Logout'
]
}
});
// Carga el nuevo estado de "homeStore", despues de
// haber sido poblado con datos.
console.log(`user: "${homeStore.state.user}"`);
// → user: "Flux"
console.log('events:', homeStore.state.events);
// → events: ["Completed chapter 1", "Completed chapter 2"]
console.log('navigation:', homeStore.state.navigation);
// → navigation: ["Home", "Settings", "Logout"]
Este es un uso bastante sencillo de nuestra store en home. Estamos registrando el estado por defecto
de la store, enviando la action HOME_LOAD con algún nuevo payload y registrando el estado de
nuevo para asegurarse de que el estado de la store realizó el cambio. Así que la pregunta es, ¿qué
tiene que ver este código con la API?
Este es un buen punto de partida para nuestra arquitectura esquelo porque hay un número de cosas
en las que pensar antes de implementar llamadas a la API. Incluso no hemos comenzado a
implementar actions, porque si lo hiciéramos, serían sólo otra distracción. Y además, las actions y
las llamadas a API reales son fáciles de implementar una vez que demos cuerpo a nuestras stores.
La primera pregunta que viene a la mente sobre el módulo main.js es la ubicación de la llamada
dispatch() a HOME_LOAD . Aquí, estamos iniciando los datos en la store. ¿Es este el lugar
correcto para hacer esto? Cuando el módulo main.js se ejecute siempre podremos que esta tienda
esté poblada? ¿Es este el lugar donde vamos a querer iniciar los datos en todas nuestras stores? No
necesitamos respuestas inmediatas a estas preguntas, porque eso probablemente resultaría en que
nos detuviéramos en un aspecto de la arquitectura demasiado tiempo, y hay muchos otros asuntos
en los que pensar.
Por ejemplo, ¿tiene sentido el acoplamiento de nuestra store? La store home que acabamos de
implementar tiene un array navigation. Estas son simples cadenas de texto ahora mismo, pero
probablemente se convertirán en objetos. El problema mayor es que los datos de navegación
podrían ni siquiera pertenecen a esta store - otras stores probablemente van a requerir datos del
estado de navegación también. Otro ejemplo es la forma en que estamos estableciendo el nuevo
estado de la tienda usando el payload enviado. La utilización de Object.assign() es ventajosa,
porque podemos enviar el evento HOME_LOAD con un payload con un solo estado y todo seguirá
funcionando igual. La implementación de este store nos tomó muy poco tiempo, pero hemos hecho
algunas preguntas muy importantes y aprendimos una técnica poderosa para asignar el nuevo estado
de la store.
Esta es la arquitectura esqueleto, y por lo tanto no nos preocupa la mecánica de obtener realmente
los datos de la API. Nos preocupan más las actions que se envían como resultado de la llegada de
datos de la API al navegador; en este caso, es HOME_LOAD . Es la mecánica de la información que
fluye a través de las stores lo que importa en el contexto de una arquitectura esqueleto Flux. Y en
ese sentido, vamos a ampliar ligeramente las capacidades de nuestra store:
// Necesitamos el "dispatcher" para registrar nuestra store,
// y la clase "EventEmitter" que permitirá a nuestra store
// emitir “cambios” de eventos cuando el estado de la
// store cambie.
import dispatcher from '../dispatcher';
import { EventEmitter } from 'events';
// Nuestro "Home" store el cual es un "EventEmitter"
class HomeStore extends EventEmitter {
constructor() {
// Siempre necesitaremos llamar a la clase de la que extiende.
super();
// Inicia un estado por defecto. Esto
// nunca es una mala idea, en caso de que otras stores quieran
// iterar sobre los valores del array – se detendrá si
// son undefined.
this.state = {
user: '',
events: [],
navigation: []
};
// Cuando un evento "HOME_LOAD" es enviado,
// podremos asignar un "payload" a un "state", entonces podremos
// emitir un evento de “cambio”.
dispatcher.register((e) => {
switch (e.type) {
case 'HOME_LOAD':
Object.assign(this.state, e.payload);
this.emit('change', this.state);
break;
}
});
}
}
export default new HomeStore();
La store todavía hace todo lo que hacía antes, sólo que ahora la clase store hereda de
EventEmitter , y cuando se envía la acción HOME_LOAD, emite un evento change usando el
estado de la store como datos del evento. Esto nos acerca un paso más a tener un flujo de trabajo
completo, ya que las vistas ahora pueden escuchar el evento change para obtener el nuevo estado
de la store. Vamos a actualizar el código de nuestro módulo principal para ver cómo se hace esto:
// Importa el "dispatcher", y el "homeStore".
import dispatcher from './dispatcher';
import homeStore from './stores/home';
// Carga el estado por defecto de la store, antes
// de que cualquier actions se disparen.
console.log(`user: "${homeStore.state.user}"`);
// → user: ""
console.log('events:', homeStore.state.events);
// → events: []
console.log('navigation:', homeStore.state.navigation);
// → navigation: []
// El evento "change" es emitido cuando el estado de la
// store cambie.
homeStore.on('change', (state) => {
console.log(`user: "${state.user}"`);
// → user: "Flux"
console.log('events:', state.events);
// → events: ["Completed chapter 1", "Completed chapter 2"]
console.log('navigation:', state.navigation);
// → navigation: ["Home", "Settings", "Logout"]
});
// Envía un evento"HOME_LOAD", cuando puebla el
// "homeStore" con datos en el "payload" del evento.
dispatcher.dispatch({
type: 'HOME_LOAD',
payload: {
user: 'Flux',
events: [
'Completed chapter 1',
'Completed chapter 2'
],
navigation: [
'Home',
'Settings',
'Logout'
]
}
});
Esta mejora de la store en nuestra arquitectura esqueleto trae consigo aún más preguntas, a saber,
sobre la creación de listener de eventos en nuestras stores. Como puedes ver, tenemos que
asegurarnos de que el manejador está escuchando a la store antes de que se envíe cualquier action.
Todas estas preocupaciones las tenemos que abordar, y apenas hemos empezado a diseñar nuestra
arquitectura. Pasemos a cambiar el estado de los recursos de backend.
Cambiar el estado de los recursos de la API
Una vez que hayamos establecido el estado inicial del store pidiéndole algunos datos a la API, es
probable que tengamos que cambiar el estado de ese recurso backend. Esto sucede en respuesta a la
actividad del usuario. De hecho, el patrón común se parece al siguiente diagrama:

Pensemos en este patrón en el contexto de un store Flux. Ya hemos visto cómo cargar datos en una
store. En la arquitectura esqueleto que estamos construyendo, no hacemos estas llamadas a la API,
incluso si existen – focalizamos solamente en información que está siendo producida por el frontend
ahora mismo. Cuando enviamos una action que cambia el estado de un store, probablemente
tendremos que actualizar el estado de este store en respuesta a la finalización exitosa de la llamada a
la API. La verdadera pregunta es, ¿qué implica esto exactamente?
Por ejemplo, ¿la llamada que hacemos para cambiar el estado del recurso backend responde
realmente con el recurso actualizado, o responde con un mero éxito? Estos tipos de patrones API
tienen un impacto dramático en nuestro diseño porque indica la diferencia entre tener que hacer
siempre una llamada secundaria o tener los datos en la respuesta.
Veamos un poco de código. Primero, tenemos un store de usuarios como el siguiente:
import dispatcher from '../dispatcher';
import { EventEmitter } from 'events';
// Nuestro store "User" store el cual es un "EventEmitter"
class UserStore extends EventEmitter {
constructor() {
super();
this.state = {
first: '',
last: ''
};
dispatcher.register((e) => {
switch (e.type) {
// Cuando la action "USER_LOAD" es enviada,
// podremos asignar el payload al estado de esta store.
case 'USER_LOAD':
Object.assign(this.state, e.payload);
this.emit('change', this.state);
break;
// Cuando la action "USER_REMOVE" es enviada,
// necesitamos verificar si este es el usuario que va a ser
// eliminado. Si lo es, reseteamos el estado.
case 'USER_REMOVE':
if (this.state.id === e.payload) {
Object.assign(this.state, {
id: null,
first: '',
last: ''
});
this.emit('change', this.state);
}
break;
}
});
}
}
export default new UserStore();
Asumiremos que esta store de usuario es única para una página en nuestra aplicación donde sólo se
muestra un usuario individual. Ahora, implementemos una store que sea útil para rastrear el estado
de varios usuarios:
import dispatcher from '../dispatcher';
import { EventEmitter } from 'events';
// Nuestro store "UserList" el cual es un "EventEmitter"
class UserListStore extends EventEmitter {
constructor() {
super();
// Por defecto no hay usuarios en la lista.
this.state = []
dispatcher.register((e) => {
switch (e.type) {
// La action "USER_ADD" añade el "payload" al
// array de estados.
case 'USER_ADD':
this.state.push(e.payload);
this.emit('change', this.state);
break;
// La action "USER_REMOVE" tiene un identificador de usuario como
// "payload" – usado para localizar el
// usuario en un array y eliminarlo.
case 'USER_REMOVE':
let user = this.state.find(
x => x.id === e.payload);
if (user) {
this.state.splice(this.state.indexOf(user), 1);
this.emit('change', this.state);
}
break;
}
});
}
}
export default new UserListStore();
Ahora vamos a crear el módulo main.js que funcionará con estas stores. En particular, queremos
ver cómo la interacción con la API para cambiar el estado de un recurso backend influirá en el
diseño de nuestras stores:
import dispatcher from './dispatcher';
import userStore from './stores/user';
import userListStore from './stores/user-list';
// Intento de simular una API back-end que cambia el
// estado de algo. En ese caso, crea
// un nuevo recurso. La promesa devuelta resultará
// en un nuevo recurso.
function createUser() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({
id: 1,
first: 'New',
last: 'User'
});
}, 500);
});
}
// Muestra el usuario cuando "userStore" cambia.
userStore.on('change', (state) => {
console.log('changed', `"${state.first} ${state.last}"`);
});
// Muestra cuantos usuarios hay cuando "userListStore"
// cambia.
userListStore.on('change', (state) => {
console.log('users', state.length);
});
// Crea un recurso back-end, entonces envía actions
// una vez la promesa ha sido resuelta.
createUser().then((user) => {
// El usuario ha sido cargado, el "payload" es el dato devuelto.
dispatcher.dispatch({
type: 'USER_LOAD',
payload: user
});
// Añade un usuario a "userListStore", usando el dato
// devuelto.
dispatcher.dispatch({
type: 'USER_ADD',
payload: user
});
// Podemos eliminar el usuario. Esto afecta a ambas stores.
dispatcher.dispatch({
type: 'USER_REMOVE',
payload: 1
});
});
Aquí podemos ver que la función createUser() sirve como proxy para la implementación de la
API actual. Recuerda, esta es una arquitectura esquelo donde la principal preocupación es la
información construida por nuestras stores. La implementación de una función que devuelve una
promesa es perfectamente aceptable aquí porque esto es muy fácil de cambiar más tarde una vez
que empecemos a comunicarnos con la autentica API.
Estamos buscando aspectos interesantes de nuestras stores: su estado, cómo ese estado y las
dependencias entre nuestras stores. En este caso, cuando creamos el nuevo usuario, la API devuelve
un nuevo objeto. A continuación, se envía como una action USER_LOAD. Nuestro userStore
está ahora poblado. También estamos enviando una acción USER_ADD para que los nuevos datos
de usuario puedan añadirse a esta lista. Presumiblemente, estas dos stores dan servicio a diferentes
partes de nuestra aplicación, y sin embargo la misma llamada API que cambia el estado de algo en
el backend es relevante.
¿Qué podemos aprender de todo esto? Para empezar, podemos ver que la promesa va a tener que
enviar múltiples actions para múltiples stores. Esto significa que probablemente podemos esperar
más de lo mismo con llamadas a API que crean recursos. ¿Qué pasa con las llamadas que modifican
a los usuarios? ¿se parecen?
Algo que nos falta aquí es una action para actualizar el estado de un objeto usuario dentro de la
matriz de usuarios en userListStore. Alternativamente, podríamos permitir a esta store
manejar la action USER_LOAD. Cualquier enfoque está bien, es el ejercicio de construir la
arquitectura esquelo que se supone que nos ayudará a encontrar el enfoque que mejor se adapte a
nuestra necesidad. Por ejemplo, estamos enviando una sola acción USER_REMOVE y esto es
manejado fácilmente por nuestras dos stores. ¿Es este el enfoque que estamos buscando?

Actions locales
Cerraremos la sección de actions de la store con un vistazo a las actions locales. Estas son actions
que no tienen nada que ver con el API. Las actions locales son generalmente en respuesta a las
necesidades de los usuarios, y su envío tendrá un efecto visible en la interfaz de usuario. Por
ejemplo, el usuario desea cambiar la visibilidad de algún componente de la página.
La aplicación típica sólo ejecutaría una línea de jQuery para localizar el elemento en el DOM y
hacer los cambios CSS apropiados. Este tipo de cosas es del tipo en las que deberíamos empezar a
pensar durante la fase de arquitectura esquelo de nuestra aplicación. Vamos a implementar una store
simple que maneja actions locales:
import dispatcher from '../dispatcher';
import { EventEmitter } from 'events';
// Nuestra store "Panel" la cual es una "EventEmitter"
class PanelStore extends EventEmitter {
constructor() {
// Siempre necesitamos hacer esta llamada si extendemos de una clase.
super();
// Estado inicial de la store.
this.state = {
visible: true,
items: [
{ name: 'First', selected: false },
{ name: 'Second', selected: false }
]
};
dispatcher.register((e) => {
switch (e.type) {
// Cambia la visibilidad del panel, el cual es
// visible por defecto.
case 'PANEL_TOGGLE':
this.state.visible = !this.state.visible;
this.emit('change', this.state);
break;
// Selecciona un objeto de los "items", sólo
// si el panel está visible.
case 'ITEM_SELECT':
let item = this.state.items[e.payload];
if (this.state.visible && item) {
item.selected = true;
this.emit('change', this.state);
}
break;
}
});
}
}
export default new PanelStore();
La action PANEL_TOGGLE e ITEM_SELECT son dos actions locales manejadas por esta store.
Son locales porque es probable que se activen cuando el usuario hace clic en un botón o selecciona
un checkbox. Vamos a enviar estas actions para que podamos ver cómo las maneja nuestra store:
import dispatcher from './dispatcher';
import panelStore from './stores/panel';
// Cambia el estado de "panelStore" cuando cambia.
panelStore.on('change', (state) => {
console.log('visible', state.visible);
console.log('selected', state.items.filter(
x => x.selected));
});
// Seleccionará el primer item.
dispatcher.dispatch({
type: 'ITEM_SELECT',
payload: 0
});
// → visible true
// → selected [ { name: First, selected: true } ]
// Deshabilita el panel cambiando el valor
// de la propiedad “visible”.
dispatcher.dispatch({ type: 'PANEL_TOGGLE' });
// → visible false
// → selected [ { name: First, selected: true } ]
// Ningún segundo item está actualmente seleccionado,
// porque el panel está deshabilidado. Ningún evento “change”
// es emitido aquí tampoco, porque la propiedad
// “visible” es false.
dispatcher.dispatch({
type: 'ITEM_SELECT',
payload: 1
});
Este ejemplo sirve como ilustración de por qué debemos considerar todas las cosas relacionadas con
el estado durante la fase de implementación de la arquitectura esqueleto. Sólo porque no estamos
implementando componentes reales de UI en este momento, no significa que no podamos adivinar
algunos de los estados potenciales de los bloques comunes. En este código, hemos descubrierto que
la action ITEM_SELECT es realmente dependiente de la action PANEL_TOGGLE. Esto se debe a
que en realidad no queremos seleccionar un elemento y actualizar la vista cuando el panel está
desactivado.
Basándose en esta idea, ¿deberían otros componentes estar capacitados a enviar esta action?
Acabamos de encontrar una posible dependencia de store, donde la tienda dependiente consultaría
el estado de panelStore antes de habilitar los elementos de la interfaz de usuario. Todo esto
desde actions locales que ni siquiera hablan con APIs, y sin elementos reales de una interfaz de
usuario. Probablemente vamos a encontrar muchos más items como este a lo largo de la de nuestra
arquitectura esqueleto, pero no te obsesiones en encontrarlos todos. La idea es aprender lo que
podamos, mientras tengamos la oportunidad de hacerlo, porque una vez empezamos a implementar
reales, las cosas se vuelven más complicadas.
Stores y dominios
Con las arquitecturas frontend más tradicionales, los modelos se mapean directamente con lo que la
API proporciona de forma clara y concisa para que nuestros componentes JavaScript trabajen. Flus,
como sabemos ahora, se inclina más en la dirección de centrarse en la información que necesita
para ver e interactuar. Esto no necesita ser un gigantesco dolor de cabeza para nosotros,
especialmente si somos capaces de descomponer nuestra interfaz de usuario en dominios. Piensa en
un dominio como una gran funcionalidad.
En esta sección, hablaremos sobre la identificación de las de nivel superior que forman el núcleo de
nuestra interfaz de usuario. Luego, trabajaremos en eliminar los datos irrelevantes de la API.
Terminaremos la sección con una mirada a la estructura de los datos de nuestra store, y el rol que
juega en el diseño de nuestra arquitectura esquelo.

Identificación de alto nivel


Durante la fase de arquitectura esqueleto de nuestro proyecto Flux, debemos dar el salto y empezar
a escribir código de store, tal y como hemos hecho en este capítulo. Hemos estado pensando en la
información que el usuario va a necesitar y cómo podemos ofrecer mejor esta información a el
usuario. Algo en lo que no pasamos mucho tiempo por adelantado fue en tratar de identificar las de
nivel superior de la aplicación. Esto está bien, porque los ejercicios que hemos realizado hasta ahora
en este capítulo son a menudo un prerrequisito para averiguar cómo organizar nuestra interfaz de
usuario.
Sin embargo, una vez que hayamos identificado cómo vamos a implementar algunos de los
mecanismos de bajo nivel de store que nos dan la información que buscamos en nuestros proyectos,
tenemos que empezar a pensar sobre estas de alto nivel. Y hay una buena razón para esto, en última
instancia, los stores mapearan dichas . Cuando decimos de alto nivel, es tentador utilizar la
navegación como punto de referencia. En realidad no hay nada malo con usar la navegación de la
página como guía; si es lo suficientemente grande la navegación, es probablemente una
funcionalidad de alto nivel que es digna de su propia store Flux.
Además de ser una funcionalidad de alto nivel, tenemos que pensar en el papel de la store. ¿Por qué
existe? ¿Qué valor añade esto para el usuario? La razón por la que estas preguntas son importantes
es porque podríamos terminar teniendo seis páginas que podrían haber usado la misma store. Así
que es un equilibrio entre consolidar el valor en una store y asegurarse de que la tienda no sea
demasiado grande y polivalente.
Las aplicaciones son complejas, con muchas piezas móviles que impulsan muchas funcionalidades.
Nuestra interfaz de usuario probablemente tiene 50 funcionalidades increíbles. Pero es poco
probable que esto requiera 50 increíbles enlaces de navegación de primer nivel y 50 stores Flux.
Nuestras stores tendrán que representar las complejidades de estas funcionalidades en sus datos, en
algún momento. Esto viene más tarde sin embargo, por ahora sólo tenemos que obtener una idea de
aproximadamente cuántos son con los que estamos trabajando, y cuántas dependencias tenemos
entre ellas.

Datos de la API no relevantes


Úsalo o déjalo: el mantra de los datos de almacenamiento de Flux. El reto con los datos de API es
que es una representación de un recurso backend - no va a devolver datos que son específicamente
requeridos por nuestra interfaz de usuario. Una API existe para que se pueda crear más de una
interfaz de usuario en ella. Sin embargo, esto significa que a menudo terminamos con datos
irrelevantes en nuestras stores. Por ejemplo, si sólo necesitamos unas pocas propiedades de un
recurso API, no queremos almacenar 36 propiedades. Especialmente cuando algunas de estas
pueden ser arrays. Esto es derrochador en términos de consumo de memoria, y confuso en términos
de su existencia. En realidad es este último punto el que es más preocupante porque podemos
fácilmente engañar a otros programadores que trabajan en este proyecto.
Una posible solución es excluir estos valores no utilizados de la respuesta de la API. Muchas APIs
hoy en día soportan esto, permitiéndonos optar por las propiedades que queremos devolver. Y esto
es probablemente una buena idea si significa una reducción drástica del ancho de banda de la red.
Sin embargo, este enfoque también puede ser propenso a errores porque tenemos que realizar este
filtrado a nivel de llamada ajax, en lugar de a nivel de store. Veamos un ejemplo que adopta un
enfoque diferente, especificando un store de registro de jugadores:
import dispatcher from '../dispatcher';
import { EventEmitter } from 'events';
class PlayerStore extends EventEmitter {
constructor() {
super();
// Las keys en el estado por defecto son
// usadas para determinar las propiedades admisibles
// usadas para asignar al estado.
this.state = {
id: null,
name: ''
};
dispatcher.register((e) => {
switch (e.type) {
case 'PLAYER_LOAD':
// Asegura que nosotros solo cogemos los datos payload
// que son propiedades de estado.
for (let key in this.state) {
this.state[key] = e.payload[key];
}
this.emit('change', this.state);
break;
}
});
}
}
export default new PlayerStore();
En este ejemplo, el objeto de estado predeterminado desempeña un papel importante, además de
proporcionar valores de estado predeterminados. También proporciona el registro de la store. En
otras palabras, las keys utilizadas por defecto determinan los valores permitidos cuando se envía la
action PLAYER_LOAD. Veamos si esto funciona como se espera:
import dispatcher from './dispatcher';
import playerStore from './stores/player';
// Registra el estado del ‘player store’ cuando cambia.
playerStore.on('change', (state) => {
console.log('state', state);
});
// Envía una action "PLAYER_LOAD" con más datos
// payload de los que actualmente utiliza la store.
dispatcher.dispatch({
type: 'PLAYER_LOAD',
payload: {
id: 1,
name: 'Mario',
score: 527,
rank: 12
}
});
// → state {id: 1, name: "Mario"}

Estructuración de datos de store


Todos los ejemplos mostrados hasta ahora en este capítulo tienen objetos de estado relativamente
simples dentro de las stores. Una vez que construimos la arquitectura esqueleto, estos simples
objetos se convertirán en algo más complicado. Recuerde, el estado de una store refleja el estado de
la información que el usuario está mirando. Esto incluye el estado de algunos de los elementos de la
página.
Esto es algo que tenemos que vigilar. Sólo porque hayamos terminado el diseño de la arquitectura
esqueleto no significa que una idea se sostendrá cuando empecemos a implementar funcionalidades
más elaboradas. En otras palabras, si un estado de store se vuelve demasiado grande-demasiado
anidado y profundo, entonces es tiempo de considerar mover nuestras store un poco.
La idea es que no queremos que demasiadas stores administren nuestras vista, porque serían más
como modelos de una arquitectura MVC en este momento. Queremos que las stores representen una
funcionalidad específica de la aplicación. Esto no siempre funciona, porque podríamos terminar
teniendo algún estado complejo y enrevesado en la store para una funcionalidad. En este caso,
nuestra funcionalidad de alto nivel necesita ser dividida de alguna manera.
Esto sin duda sucederá en algún momento durante nuestra integración con Flux, y no hay nada que
establezca cuándo es el momento de refactorizar las stores. En cambio, si los datos del estado
permanecen a un tamaño en el que se sienta cómodo para trabajar, es probable que sea compatible
con la store como tal.

Vistas básicas
Hemos hecho algunos progresos con nuestras store esqueleto hasta el punto de que estamos listos
para empezar a mirar vistas esqueleto. Son clases sencillas, con el mismo espíritu que las stores,
excepto que en realidad no le daremos nada al DOM. La idea con estas vistas básicas es afirmar la
infraestructura de nuestra arquitectura, y que estos componentes de la vista están obteniendo de
hecho la información que esperan. Esto es crucial porque las vistas son el elemento final en el flujo
de datos de Flux, así que si no logran conseguir lo que necesitan, cuando lo necesitan, tenemos que
volver y arreglar nuestras stores.
En esta sección, discutiremos cómo nuestras vistas básicas pueden ayudarnos más rápidamente a
identificar cuando a las stores les falta una información en particular. Entonces, buscaremos sobre
cómo estas vistas pueden ayudarnos a identificar actions potenciales en nuestra aplicación Flux.

Búsqueda de datos perdidos


La primera actividad que llevaremos a cabo con nuestras vistas básicas es averiguar si las stores no
están pasando toda la información esencial requerida por la vista. Por esencial, estamos hablando de
cosas que serían problemáticas para el usuario si no están allí. Por ejemplo, estamos viendo una
página de configuración, y hay una página completa de sección desaparecida. O bien, hay una lista
de opciones para seleccionar, pero en realidad no tenemos las etiquetas a mostrar porque son parte
de alguna otra API.
Una vez que descubrimos que estas piezas críticas de información faltan el siguiente paso es
determinar si son una eventualidad, porque si no lo son, hemos evitado pasar una cantidad
desmesurada de tiempo implementando una vista de pleno derecho. Sin embargo, estos son casos
raros. Por lo general, no es gran cosa ir de vuelta a la store en cuestión y añadir la transformación
que falta que computará y establecerá el estado perdido que estamos buscando.
¿Cuánto tiempo necesitamos invertir en estas vistas? Piénsalo de esta manera, a medida que
empezamos a implementar las vistas reales que le entregamos al DOM, descubriremos más estados
perdidos en la store. Estos, sin embargo, son superficiales y fáciles de arreglar. Con las vistas
básicas, estamos más preocupados por descifrar las partes críticas que faltan. ¿Qué podemos hacer
con estas vistas cuando terminemos con ellas? ¿Son basura? No necesariamente, dependiendo de
cómo queramos implementar nuestras vistas en producción, podríamos ajustarlas para convertirlas
en ReactJS o podríamos incrustar la vista real dentro de la vista desnuda, convirtiéndolas en un
contenedor.

Identificación de actions
Como vimos anteriormente en el capítulo, el primer conjunto de actions a ser enviadas por una
determinada arquitectura Flux va a estar relacionada con la obtención de datos de la API de
backend. En última instancia, estos son el comienzo de los flujos de datos que terminan en las
vistas. A veces, estas son meramente actions de tipo carga, donde estamos diciendo explícitamente
lo que vamos a buscar y poblar con ello nuestra store. Otras veces, podríamos tener actions más
abstractas que describen la acción tomada por el usuario, resultando en que varias stores sean
pobladas desde muchos endpoints de API diferentes.
Esto lleva al usuario al punto donde queremos empezar a pensar como van a querer interactuar con
la información. La única manera de hacerlo es enviando más actions. Vamos a crear una vista con
algunos métodos action. Esencialmente, el objetivo es tener acceso a nuestras vistas desde la
consola JavaScript del navegador. Esto nos permite ver la información de estado asociada a la vista
en un punto dado, así como llamar a la función método para enviar la action dada.
Para hacer esto, necesitamos ajustar ligeramente la configuración de nuestro Webpack:
output: {
...
library: 'views'
}
Esta línea exportará una variable de vistas globales en la ventana del navegador, y su valor será
cualquiera que sea la exportación de nuestro módulo main.js. Echemos un vistazo a esto ahora:
import settingsView from './views/settings';
export { settingsView as settings };
Bueno, esto parece interesante. Simplemente estamos exportando nuestra vista como settings.
Por lo tanto, a medida que creamos nuestras vistas básicas en la arquitectura esqueleto, simplemente
seguimos este patrón en main.js para seguir añadiendo vistas a la consola del navegador con las
que experimentar. Ahora echemos un vistazo a la propia vista settings:
import dispatcher from '../dispatcher';
import settingsStore from '../stores/settings';
// Esta es una vista “básica” porque
// no renderiza nada al DOM. Solo la estamos
// usando para validar nuestro flujo de datos Flux y
// pensar sobre las actions potenciales enviadas
// desde esta vista.
class SettingsView {
constructor() {
// Registra el estado de "settingsStore" cuando
// cambia.
settingsStore.on('change', (state) => {
console.log('settings', state);
});
// El estado inicial de la store es registrado.
console.log('settings', settingsStore.state);
}
// Esto asigna un valor email para ser enviado
// a una action "SET_EMAIL".
setEmail(email) {
dispatcher.dispatch({
type: 'SET_EMAIL',
payload: 'foo@bar.com'
});
}
// ¡Hacer todo!
doTheThings() {
dispatcher.dispatch({
type: 'DO_THE_THINGS',
payload: true
})
}
}
// No necesitamos más que una de estas
// vistas, así que exportes una nueva instancia.
export default new SettingsView();

Escenarios de extremo a extremo


En algún momento, vamos a tener que concluir la fase de arquitectura esqueleto y empezar a
implementar funcionalidades reales. No queremos que la fase esqueleto nos lleve demasiado tiempo
porque entonces empezaremos a hacer demasiadas suposiciones sobre la realidad de nuestra
implementación. Al mismo tiempo, probablemente querremos recorrer unos cuantos escenarios de
principio a fin antes de seguir adelante.
El objetivo de esta sección es proporcionarle algunos puntos de alto nivel para buscar en cada capa
arquitectónica. Estos no son criterios estrictos, pero pueden ciertamente ayudarnos a formular
nuestras propias medidas que determinan si hemos respondido adecuadamente a nuestras preguntas
sobre la arquitectura de información mediante la construcción de un esqueleto. Si nos sentimos
seguros, es hora de ir a toda máquina y dar cuerpo el detalle de la aplicación - los capítulos
siguientes de este libro se adentran en el meollo de la cuestión de implementar Flux.

Lista de actions
Vale la pena pensar en los siguientes puntos cuando estamos implementando acciones:
• ¿Nuestra funciones tienen actions que inician el almacenamiento de datos obteniéndolos de
una API?
• ¿Tenemos acciones que cambien el estado de los recursos de backend? ¿Como están
reflejados estos cambios en nuestras stores de frontend Flux?
• ¿Tiene una funcionalidad determinada alguna action local, y es distinta a las actions que
envían solicitudes a la API?

Lista de store
Los siguientes puntos vale la pena pensarlos cuando implementamos stores:
• ¿Es el store una funcionalidad de alto nivel en nuestra aplicación?
• ¿Cómo de bien suplen las necesidades de las vistas la estructura de datos de nuestra store?
¿Es la estructura demasiado compleja? Si es así, ¿podemos refactorizar la store en dos?
• ¿Descarta la store datos de la API que no usa?
• ¿Hacen los stores mapeo de datos de la API relevantes para las necesidades del usuario?
• ¿Es susceptible de cambio la estructura de nuestra store una vez que empecemos a añadir
vistas con funcionalidades más elaboradas?
• ¿Tenemos demasiadas stores? Si es así, ¿necesitamos replantear el camino en que hemos
estructurado las funcionalidades de alto nivel?

Lista de vista
Los siguientes puntos vale la pena pensarlos cuando implementamos vistas:
• ¿Obtiene la vista la información que necesita de la store?
• ¿Qué actions actúan sobre el renderizado de la vista?
• ¿Qué actions envía la vistas en respuesta de la interacción del usuario?

Sumario
Este capítulo trataba sobre cómo empezar con una arquitectura Flux construyendo algunos
componentes del esqueleto. El objetivo es pensar en la arquitectura de la información, sin la
distracción de otros problemas de implementación. Podríamos encontrarnos en una situación en la
que la API ya está definida para nosotros, o en la que la experiencia del usuario ya está en su lugar.
Cualquiera de estos factores influirá en el diseño de nuestras stores, y en última instancia, la
información que presentamos a nuestros usuarios.
Los stores que implementamos eran básicos, cargando los datos cuando se inicia la aplicación y
actualizando su estado en respuesta a una llamada de API. Sin embargo, aprendimos a a hacerle las
preguntas pertinentes acerca de nuestras stores, como el enfoque adoptado para analizar los nuevos
datos a establecer como estado de la store, y cómo afectará este nuevo estado a otras stores.
Luego, pensamos en las funcionalidades de alto nivel que forman el núcleo de nuestra aplicación.
Estas funcionalidades dan una buena indicación de las stores que nuestra arquitectura necesitará.
Hacia el final de la fase de arquitectura esqueleto, queremos recorrer unos pocos escenarios de
principio a fin para comprobar el diseño de la información que hemos elegido. Nos fijamos en
algunos elementos de la lista de alto nivel para ayudar a asegurarnos de que no dejamos nada
importante fuera. En el siguiente capítulo, veremos más a fondo las actions y cómo son enviadas.
4
Creando Actions
En el capítulo anterior, trabajamos en la construcción de una arquitectura esqueleto para nuestra
aplicación Flux. Las actions fueron enviadas directamente por el dispatcher. Ahora que tenemos una
arquitectura esqueleto Flux bajo nuestro control, es hora de mirar más profundamente las actions y,
en particular, cómo se crean.
Empezaremos hablando de los nombres que damos a las actions y de las constantes utilizadas para
identificar las actions disponibles en nuestro sistema. Entonces, implementaremos alguna action y
pensaremos en cómo podemos mantener estas funciones modulares. Incluso aunque hayamos
terminado de implementar nuestra arquitectura esqueleto, podemos todavía tener la necesidad de
imitar algunos datos de la API - vamos a repasar cómo se hace esto con la función creadora de
actions.
Las funciones típicas del creador de actions son sin estado-entrada de datos, salida de datos.
Cubriremos algunos escenarios en los que los creadores de la action dependen realmente del estado,
como cuando haya involucradas conexiones. Terminaremos el capítulo con un vistazo a la
parametrización de actions, lo que nos permite reutilizarlas para diferentes propósitos.

Nombres y constantes de actions


Cualquier aplicación grande de Flux tendrá muchas actions. Es por eso que tener constantes action y
nombres de actions sensatos son importantes. El enfoque de esta sección es discutir posibles
convenciones de nomenclatura para actions y para organizarse con nuestras actions. Las constantes
ayudan a reducir los strings repetitivos que son propensos a errores, pero también vamos a necesitar
pensar en la mejor manera de organizar nuestras constantes. También buscaremos en los datos
estáticos de la action-esto también nos ayudará a reducir la cantidad código que tenemos escribir.

Convenciones de nombres de acciones


Todas las actions en un sistema Flux tienen un nombre. El nombre es importante porque dice a
quienquiera que lo esté viendo mucho sobre lo que hace. Una aplicación donde hay menos es
improbable que más de diez acciones tengan un fuerte requisito de convención de nomenclatura,
debido a que fácilmente podemos averiguar qué hacen estas acciones. Sin embargo, es igualmente
improbable que use Flux para implementar una pequeña aplicación - Flux es para sistemas que
necesitan escalar. Esto significa que hay una gran probabilidad de muchas actions.
Los nombres de las actions se pueden dividir en dos segmentos: el asunto y la operación. Por
ejemplo, tener una action llamada ACTIVATE no sería de mucha ayuda-¿qué es lo que estamos
activando? La adición de un tema al nombre es a menudo todo lo que se necesita para proporcionar
algo de contexto necesario. He aquí algunos ejemplos:
• ACTIVATE_SETTING
• ACTIVATE_USER
• ACTIVATE_TAB
El asunto es algo abstracto en nuestro sistema, ni siquiera tiene que corresponder a una entidad de
software concreta. Sin embargo, si hay muchos asuntos en nuestro sistema con actions similares, es
posible que queramos cambiar el formato de nuestros nombres de action, como por ejemplo este:
• SETTING_ACTIVATE
• USER_ACTIVATE
• TAB_ACTIVATE
Al final, esto es realmente una preferencia personal (o del equipo), siempre y cuando el nombre sea
lo suficientemente descriptivo como para proporcionar significado a alguien que esté mirando el
código. ¿Y si el asunto y la operación no son suficientes? Por ejemplo, podría haber varios temas
que son similares, y esto podría causar confusión. Entonces, podríamos añadir otra capa de asunto al
nombre, pensar en esto como un espacio de nombres para la action.
Intente no ir más allá de tres segmentos en un nombre de la action en Flux. Si siente la
necesidad de hacer esto, probablemente haya algún otro lugar en su arquitectura que
necesite atención.

Datos estáticos de action


Algunas actions son muy similares a otras, similares en el sentido de que los datos de carga útil que
se envían a las stores tienen muchas de las mismas propiedades. Si tuviéramos que enviar
directamente estas actions, usando la instancia del dispatcher, entonces normalmente tendríamos
que repetir el código literal del objeto. Echemos un vistazo a una función de creador de actions:
import dispatcher from '../dispatcher';
// Nombre de la action.
export const SORT_USERS = 'SORT_USERS';
// Este función creadora de action hard-codea
// el payload de la action.
export function sortUsers() {
dispatcher.dispatch({
type: SORT_USERS,
payload: {
direction: 'asc'
}
});
}
El objetivo de esta action es bastante sencillo: ordenar la lista de usuarios que son presumiblemente
componentes de la interfaz de usuario. La única información de payload que se requiere es una
dirección de ordenación, que se especifica en la propiedad direction. El problema con este
creador de actions es que los datos del payload están codificados. Por ejemplo, los datos del payload
mostrados aquí parecen bastante genéricos, y otras funciones de creación de actions que ordenan los
datos debería seguir este patrón. Pero, esto también significa que cada uno tendrá sus propios
valores codificados.
Una cosa que podemos hacer al respecto es crear un módulo dentro del directorio de actions que
exporta cualquier dato de payload por defecto que puede ser compartido entre varias funciones de
creación de actions. Continuando con el ejemplo de ordenación, el módulo podría verse algo como
esto:
// Este objeto es usado en muchas funciones
// creadoras de actions como parte del payload
// de una action.
export const PAYLOAD_SORT = {
direction: 'asc'
};
Esto es fácil de construir. Podemos ampliar PAYLOAD_SORT , a medida que se necesiten nuevas
propiedades y cuando sea necesario cambiar los valores predeterminados antiguos. También es fácil
añadir nuevos payloads predeterminados a medida que se necesiten. Echemos un vistazo a otra
función de creación de actions que utiliza este payload predeterminado:
import dispatcher from '../dispatcher';
import { PAYLOAD_SORT } from './payload-defaults';
// Nombre de action.
export const SORT_TASKS = 'SORT_TASKS';
// Esta función de creación de actions usa
// el objeto por defecto "PAYLOAD_SORT" como
// payload.
export function sortTasks() {
dispatcher.dispatch({
type: SORT_TASKS,
payload: PAYLOAD_SORT
});
}
Como podemos ver, el objeto PAYLOAD_SORT es utilizado por la función sortTasks(), en
lugar de codificar el payload dentro del creador de la action. Esto reduce la cantidad de código que
necesitamos escribir, y coloca los datos comunes del payload en un lugar central, lo que nos facilita
cambiar el comportamiento de muchas funciones de creación de actions.
Usted puede haber notado que el objeto payload por defecto está siendo pasado a
dispatch() tal cual. La mayoría de las veces, tendremos parte del objeto payload
que es común a varias funciones y parte del objeto payload que es dinámico.
Incorporaremos los ejemplos de esta sección en la última sección del capítulo,
cuando llegue el momento de pensar en las funciones parametrizadas de creación de
actions.
Ahora, echemos un vistazo a estas dos funciones de creación de actions en uso, para asegurarnos de
que estamos obteniendo lo que esperamos. En lugar de establecer stores para esto, sólo
escucharemos al dispatcher directamente:
import dispatcher from './dispatcher';
// Obtiene la constante de action y la función creadora
// para "SORT_USERS".
import {
SORT_USERS,
sortUsers
} from './actions/sort-users';
// Obtiene la constante action y la función creadora
// para "SORT_TASKS".
import {
SORT_TASKS,
sortTasks
} from './actions/sort-tasks';
// Escucha actions, y registra alguna información
// dependiendo que action fue enviada.
// Observa que estamos usando la nombre de la constante de la action
// aquí, así tendremos menos posibilidad de errores humanos.
dispatcher.register((e) => {
switch (e.type) {
case SORT_USERS:
console.log(`Sorting users "${e.payload.direction}"`);
break;
case SORT_TASKS:
console.log(`Sorting tasks "${e.payload.direction}"`);
break;
}
});
sortUsers();
// → Sorting users "asc"
sortTasks();
// → Sorting tasks "asc"

Organizar constantes de action


Puede que hayas notado que ya hay un indicio de organización con las actions utilizadas en el
ejemplo anterior. Por ejemplo, la constante SORT_USERS se definió en el mismo módulo que la
función creadora de actions sortUsers(). Esto es generalmente una buena idea porque estas dos
cosas están estrechamente relacionadas entre sí. Sin embargo, esto tiene un inconveniente. Imagine
una store más compleja que necesita manejar un montón de actions. Si cada constante de action
individual se declara en su propia la store tendría que realizar muchas importaciones sólo para
obtener estas constantes. Si hay un número de stores complejas que cada una necesita acceso a un
montón de actions, el número de importaciones empieza a aumentar. Este problema se ilustra aquí:
import { ACTION_A } from '../actions/action-a';
import { ACTION_B } from '../actions/action-b';
import { ACTION_C } from '../actions/action-c';
// …
Si nos encontramos en una situación como esta, en la que varias stores necesitan acceder a varios
módulos, tal vez necesitemos un módulo constants.js en el directorio actions. Este módulo
expondría cada action en el sistema. He aquí un ejemplo de cómo podría ser este módulo:
export { ACTION_A } from './action-a';
export { ACTION_B } from './action-b';
export { ACTION_C } from './action-c';
A medida que nuestro sistema crece y se agregan nuevas actions, aquí es donde centralizaríamos las
constantes action para facilitar el acceso a las stores que requieren de muchas de ellas. No están
definidas aquí; esto es sólo un proxy que reduce el número de importaciones en stores, porque las
stores nunca necesitan las funciones creadoras de actions. Veamos si la situación ha mejorado desde
la perspectiva de una store que requiere constantes de actions:
import {
ACTION_A,
ACTION_B,
ACTION_C
} from './actions/constants';
console.log(ACTION_A);
// → ACTION_A
console.log(ACTION_B);
// → ACTION_B
console.log(ACTION_C);
// → ACTION_C
Así está mejor. Sólo una declaración de importación nos da todo lo que necesitamos, y sigue siendo
agradable y legible. Hay varias maneras en las que podemos hacer que este enfoque se adapte mejor
a nuestras necesidades. Por ejemplo, quizás en lugar de un gran módulo de constantes, queremos
agrupar nuestras actions en módulos lógicos que se asemejen más a nuestras funcionalidades, y del
mismo modo para nuestras funciones creadoras de actions. Discutiremos la modularidad de la
action en lo que se refiere a nuestras funcionalidades de aplicación en la siguiente sección.

Creadores de acciones
Las funciones de creación de actions necesitan estar organizadas, tal como lo están las constantes de
action. En los ejemplos de código anteriores de este capítulo, hemos organizado nuestras constantes
de action y nuestra función creadora de actions en módulos. Esto mantiene nuestro código de action
limpio y fácil de cruzar. En esta sección, nos basaremos en esta idea desde el punto de vista de las
funcionalidades de vista. Vamos a ver por qué vale la pena pensar en esto en primer lugar, luego
hablaremos sobre cómo estas ideas hacen que la arquitectura en su conjunto sea más modular.

Cuando se necesita modularidad


¿Tenemos que pensar profundamente en la modularidad de las funciones creadoras de actions desde
el inicio de nuestro proyecto Flux? Aunque el proyecto sigue siendo pequeño en tamaño, está bien
si todos los creadores de actions son parte de un módulo creador de actions monolítico -
simplemente no hay impacto significativo en la arquitectura. Es cuando tenemos más de una docena
o así de acciones cuando necesitamos empezar a pensar en la modularidad y, en particular, en las
funcionalidades.
Podemos dividir nuestro módulo de creación de actions en varios módulos más pequeños, cada uno
con su propia función de creación de actions. Esto es ciertamente un paso en la dirección correcta,
pero en esencia, estamos moviendo el problema al nivel de directorio. Así que en vez de un módulo
monolítico, ahora tenemos un directorio monolítico con muchos archivos en él. Este directorio se
ilustra aquí:

No hay nada intrínsecamente malo en este diseño, sólo que no hay indicación de qué funcionalidad
forma parte una determinada action. Esto puede que ni siquiera sea necesario, pero cuando la
arquitectura crece un cierto tamaño, suele ser útil agrupar los módulos de acción por
funcionalidades. Este concepto se ilustra aquí:

Una vez que seamos capaces de organizar las actions del sistema de tal manera que reflejen el
comportamiento de una funcionalidad determinada, podremos empezar a pensar en otros retos
arquitectónicos relacionados con la modularidad. Discutiremos esto a continuación.

Arquitectura modular
Es bueno que los módulos en una arquitectura Flux empiecen a tomar la forma de las
funcionalidades que ofrece nuestra aplicación. Esto tiene implicaciones en otras partes de la
arquitectura también. Por ejemplo, si estamos organizando las actions por funcionalidad, entonces
¿no deberíamos también organizar las stores y las vistas por funcionalidades? Las stores son fáciles,
no son exactamente descomponibles en stores más pequeñas, representan naturalmente la
funcionalidad en su la totalidad. Las vistas, por otro lado, podrían tener potencialmente muchos
módulos JavaScript para organizar dentro de una funcionalidad. Aquí hay una estructura de
directorio potencial de una funcionalidad de Flux:
Esta es una estructura cohesiva, todo lo que se necesita para enviar estas acciones están en el mismo
directorio padre. Del mismo modo, la store que notifica a las vistas sobre los cambios de estado está
en el mismo lugar. Podemos salirnos con la nuestra siguiendo un patrón similar para todas nuestras
funcionalidad, lo cual tiene el beneficio adicional de promover la consistencia.
Revisaremos la estructuración de los módulos de funcionalidades hacia el final del libro. Por ahora,
nuestra principal preocupación son las dependencias que otras funcionalidades pueden tener con un
conjunto dado de actions. Por ejemplo, nuestra funcionalidad define varias actions que son enviadas
por vistas. ¿Qué debería pasar con otras funcionalidades que quieren responder a estas actions?
¿necesitan depender de esta funcionalidad para la action? También está el asunto de los creadores de
actions mismos, y si otras funcionalidades pueden o no enviarlos. La respuesta es un sí rotundo, y la
razón es simple - las actions son cómo suceden las cosas en las arquitecturas Flux. No hay ningún
bus de eventos en el que los módulos publiquen eventos de una manera auto guiada. Las actions
juegan un papel vital en la modularidad de nuestra arquitectura Flux.

Simulación de datos
El dispatcher en arquitecturas Flux, es el único punto de entrada para la introducción de nuevos
datos en el sistema. Esto facilita la fabricación de datos simulados para ayudarnos a crear
funcionalidades, más rápido. En esta sección, discutiremos cómo simular las APIs existentes, y si se
trata o no de que valga la pena incluir en el creador de actions funciones que las comuniquen.
Entonces, iremos sobre la implementación de simulaciones para nuevas APIs que aún no existen,
seguido de un vistazo a estrategias para sustituir a los creadores de actions simuladas por reales.

Simulación de APIs existentes


Para poder simular datos en un sistema Flux, las actions que se envían necesitan entregar datos
simulados a las stores. Esto se hace creando una implementación alternativa de la función creadora
de actions que envía la action. Cuando ya existe una API a la que un creador de actions puede
dirigirse, no necesariamente necesitamos simulación de los datos durante el desarrollo de una
característica dada: los datos ya están ahí. Sin embargo, la existencia de una API utilizada por un
creador de actions no debería descartar la existencia de una versión simulada.
La razón principal por la que querríamos hacer esto es porque en cualquier momento dado durante
el vida útil de nuestro producto, va a faltar una API que necesitamos. Como veremos en la siguiente
sección, obviamente querremos simular los datos devueltos por esta API, así podemos continuar
implementando la funcionalidad en la que estamos trabajando. Pero, ¿realmente queremos simular
algunas actions y otras no? La idea se ilustra aquí:
El desafío con este enfoque - simulación de algunas acciones mientras se implementan otras - es
consistente. Cuando simulamos los datos que entran en el sistema, tenemos que ser conscientes de
las relaciones entre un conjunto de datos y otro. Míralo desde la perspectiva de nuestras stores, es
probable que dependan unas de otras. ¿Podemos capturar estas dependencias utilizando una mezcla
de datos simulados y datos reales? Aquí hay una ilustración de acciones que simulan la totalidad del
sistema:

Es mejor tener un control total sobre los datos que se utilizan cuando experimentamos con nuevas
funcionalidades. Esto elimina la posibilidad de comportamiento errante debido a que algunos datos
son inconsistentes. Se necesita más esfuerzo para construir datos simulados como éste, pero al final
resulta rentable cuando añadimos nuevas funciones y sólo tenemos que simular una nueva acción
por vez, a medida que se añade al sistema. Como veremos más adelante en esta sección, es fácil
sustituir a los creadores de actions simulados por los creadores de actions de producción.

Simulando nuevas APIs


Una vez que llegamos al punto durante la implementación de una nueva funcionalidad donde hemos
perdido la implementación de la API, tendremos que simularla. Podemos usar este nuevo simulacro
con las otras simulaciones que hemos creado para soportar otras funcionalidades de la aplicación.
La ventaja de hacer esto es que nos permite crear algo sin demora, algo que podamos demostrar a
las partes interesadas. Otra ventaja de simular las APIs como funciones creadoras de actions es que
pueden ayudar a dirigir la API en la dirección correcta. Sin una interfaz de usuario, la API no tiene
nada en que basar su diseño, así que esta es una buena oportunidad para solicitar un diseño que
funcione mejor con la aplicación que estamos construyendo.
Echemos un vistazo a algunas funciones de creación de actions que simulan los datos que se envían
como payload de action. Comenzaremos con una función básica de arranque que inicializará
algunos datos en la store por nosotros:
import dispatcher from '../dispatcher';
// Indentificación de la action....
export const LOAD_TASKS = 'LOAD_TASKS';
// Inmediatamente envía la action usando un array
// de objetos tarea como datos simulados.
export function loadTasks() {
dispatcher.dispatch({
type: LOAD_TASKS,
payload: [
{ id: 1, name: 'Task 1', state: 'running' },
{ id: 2, name: 'Task 2', state: 'queued' },
{ id: 3, name: 'Task 3', state: 'finished'}
]
});
}
Esto es muy sencillo. Los datos que queremos simular son parte de la función, como payload de la
action. Veamos otro creador de action simulada ahora, uno que manipula el estado de una store
después de que los datos ya han sido iniciados:
import dispatcher from '../dispatcher';
// Identidad de la action....
export const RUN_TASK = 'RUN_TASK';
// Usa "setTimeout()" para simular la latencia que
// veríamos en una petición real.
export function runTask() {
setTimeout(() => {
dispatcher.dispatch({
type: RUN_TASK,
// Datos simulados muy específicos. Estos
// datos simulados no tienen necesariamente que
// estar hard-codeados como estos,pero esto hace
// la experimentación más sencilla..
payload: {
id: 2,
state: 'running'
}
});
}, 1000);
}
Una vez más, tenemos datos simulados muy específicos que estamos usando aquí, lo que está bien
porque está directamente acoplado a la función de creación de action que está enviando la action -
esta es la única manera en que estos datos pueden entrar en el sistema también. Otra cosa que es
diferente en esta función es que simula la latencia al no enviar la action hasta que la llamada de
retorno setTimeout() se activa después de un segundo.
En un capítulo posterior analizaremos más detalladamente las actions asíncronas,
incluyendo latencia, promesas y múltiples endpoints de API.
En este punto, tenemos dos funciones del creación de la action simuladas disponibles para su
uso. Pero antes de empezar a usar estas funciones, vamos a crear un store de tareas para que
podamos asegurarnos de que está siendo almacenada la información correcta:
import EventEmitter from 'events';
import dispatcher from '../dispatcher';
import { LOAD_TASKS } from '../actions/load-tasks';
import { RUN_TASK } from '../actions/run-task';

// El store para las tareas mostradas en la aplicación.


class TaskStore extends EventEmitter {
constructor() {
super();
this.state = [];
dispatcher.register((e) => {
switch(e.type) {
// En caso de "LOAD_TASKS", podemos usar el
// "payload" como un nuevo estado del store.
case LOAD_TASKS:
this.state = e.payload;
this.emit('change', this.state);
break;
// En caso de "RUN_TASK", necesitamos buscar
// un objeto tarea específica y cambiar su estado.
case RUN_TASK:
let task = this.state.find(
x =>x.id === e.payload.id);
task.state = e.payload.state;
this.emit('change', this.state);
break;
}
});
}
}
export default new TaskStore();
Ahora que tenemos una store para manejar las dos actions que acabamos de implementar, pongamos
la store y las actions a usar en el módulo main.js de la aplicación:
import taskStore from './stores/task';
import { loadTasks } from './actions/load-tasks';
import { runTask } from './actions/run-task';
// Registra el stado del store, como un array mapeado
// de strings.
taskStore.on('change', (state) => {
console.log('tasks',
state.map(x => `${x.name} (${x.state})`));
});
loadTasks();
// →
// tasks [
// "Task 1 (running)",
// "Task 2 (queued)",
// "Task 3 (finished)"
// ]
runTask();
// →
// tasks [
// "Task 1 (running)",
// "Task 2 (running)",
// "Task 3 (finished)"
// ]
Como puede ver, las tareas se iniciaron con éxito en la store con la llamada a loadTasks() , y el
estado de la segunda tarea se actualizó cuando llamamos a runTask(). Esta última actualización
no se registra hasta que haya transcurrido un segundo.

Sustitución de los creadores de acciones


En este punto, tenemos funcionando una función de creación de actions que envía actions con datos
simulados en el payload al sistema. Recordemos que no necesariamente queremos deshacernos de
estas funciones de creación de actions, porque cuando llegue el momento de implementar algo
nuevo, querremos usar estas simulaciones de nuevo.
Lo que realmente necesitamos es un interruptor global que conmute el modo simulado del sistema,
y esto cambiaría la implementación de la función de creación de actions que se utiliza. He aquí un
diagrama que muestra cómo podría funcionar esto:

La idea aquí es que hay una versión simulada y una versión de producción de la misma función de
creación de actions dentro del módulo. Esta es la parte fácil; la parte difícil va a ser la
implementación de un interruptor simulado global para que se exporte la función correcta,
dependiendo del modo de la aplicación:
import { MOCK } from '../settings';
import dispatcher from '../dispatcher';
// Identificación de la action...
export const LOAD_USERS = 'LOAD_USERS';
// La implementación simulada del creador de action.
function mockLoadUsers() {
dispatcher.dispatch({
type: LOAD_USERS,
payload: [
{ id: 1, name: 'Mock 1' },
{ id: 2, name: 'Mock 2' }
]
});
}
// La implementación en producción del creador de action.
function prodLoadUsers() {
dispatcher.dispatch({
type: LOAD_USERS,
payload: [
{ id: 1, name: 'Prod 1' },
{ id: 2, name: 'Prod 2' }
]
});
}
// Aquí es donde el valor "loadUsers" es determinado, basado
// en la configuración de "MOCK". Siempre va a ser exportada
// como "loadUsers", significando que ningún otro código necesita cambiar.
const loadUsers = MOCK ? mockLoadUsers : prodLoadUsers;
export { loadUsers as loadUsers };
Esto es muy útil durante el desarrollo, porque el alcance de nuestras funciones simuladas se limita a
los módulos de creación de actions y es controlado por una configuración. Veamos cómo se utiliza
esta función de creador de actions, independientemente de si se exporta el simulacro o la
implementación de producción:
import dispatcher from './dispatcher';
// Este código nunca tiene que cambiar, a pesar de que la actual
// función que es exportada cambie, dependiendo de
// la configuracion de "MOCK".
import { loadUsers } from './actions/load-users';
dispatcher.register((e) => {
console.log('Users', e.payload.map(x =>x.name));
});
loadUsers();
// → Users ["Mock 1", "Mock 2"]
// Cuando la configuración "MOCK" es true...
// → Users ["Prod 1", "Prod 2"]

Creadores de actions con estado


Las funciones de creación de actions que hemos visto hasta ahora en este capítulo han sido
relativamente simples-envían alguna action cuando se les llama. Pero antes de que eso ocurra, estos
creadores de actions normalmente llegarán a algún endpoint de la API para recuperar algunos datos,
luego enviar la action, usando los datos como payload. Esto se llama función de creación de action
sin estado, porque no hay un estado intermediario en ellos, en otras palabras, no hay ciclo de vida.
En esta sección, pensaremos en cosas con estado y en cómo podríamos llevarlas a cabo
integrándolas en nuestra arquitectura Flux. Otro de los retos a los que nos podemos enfrentar es
integrar nuestra aplicación Flux en otra arquitectura. Primero, cubriremos algo sobre los creadores
de actions con estado, luego veremos un ejemplo concreto usando web sockets.
Integración con otros sistemas
La mayoría de las veces, las aplicaciones de Flux son independientes en el navegador. Es decir, no
son un engranaje en una máquina más grande. Sin embargo, nos encontraremos con casos en los
que nuestra arquitectura Flux necesita encajar en algo más grande. Por ejemplo, si necesitamos
interactuar con componentes que utilizan un framework completamente diferente, entonces tenemos
que tener una forma de incrustar nuestro software sin comprometer los patrones de Flux. O quizás
el acoplamiento entre nuestra aplicación y la que estamos integrando es un poco más flexible, como
cuando se comunica con otra pestaña del navegador. Cualquiera que sea el caso posible, tenemos
que ser capaces de enviar mensajes a este sistema externo y necesitamos poder consumir mensajes
de el, traduciéndolos en actions. Aquí hay un ilustración de esta idea:

Como puede ver, la arquitectura Flux representada aquí no es un sistema cerrado. La implicación
principal es que las funciones típicas de creación de actions con las que estamos acostumbrados a
trabajar no se llaman necesariamente dentro del sistema. Es decir, están manejando un flujo de
mensajes externos, usando una conexión de estado con el otro sistema. Así es como funcionan los
web sockets. A continuación, veremos a estos creadores de acciones estatales.

Conectividad de web socket


La conectividad de web socket está creciendo hasta el punto de ser omnipresente en la web
moderna, y si estamos construyendo una arquitectura Flux, es muy probable que vayamos a
necesitar construir soporte para web sockets. Cuando algo cambia de estado en el backend, los web
sockets son una gran manera de notificar a los clientes acerca de este tipo de cambio. Por ejemplo,
imagina que un store Flux está gestionando el estado de alguna parte de los datos de backend, y
algo causa que su estado cambien-¿querríamos que el store lo sepa?
El reto es que necesitamos una conexión de estado para recibir el web socket y traducirlo en actions
de Flux. Así es como entran los datos del web socket al sistema. Echemos un vistazo a algún código
listener de socket:
// Obtiene las constantes y funciones actions
// que necesitamos.
import { ONE, one } from './one';
import { TWO, two } from './two';
import { THREE, three } from './three';
var ws;
var actions = {};
// Crea un mapao de constantes a funciones
// que el manejador del web socket puede usar para llamar
// al creador de action apropiado.
actions[ONE] = one;
actions[TWO] = two;
actions[THREE] = three;
// Conecta con el web socket...
export default function connect() {
ws = new WebSocket('ws://127.0.0.1:8000');
ws.addEventListener('message', (e) => {
// Traduce el mensaje y usa el
// mapeo de "actions" para llamar a la correspondiente
// funcion de creación de action.
let data = JSON.parse(e.data);
actions[data.task](data.value);
});
}
Todo lo que estamos haciendo aquí es crear un simple mapa de actions. Así es como llamamos a la
función creadora de actions correctas basada en la propiedad task del mensaje que fue recibido.
Lo bueno de este enfoque es que hay muy poco necesario para que esto funcione; el código anterior
es su extensión. Las funciones reales de creación de actions, constantes, etc., son sólo elementos
típicos Flux. Veamos el código del servidor que genera estos mensajes de web socket, así tenemos
una idea de lo que realmente está pasando al código listener del socket:
// El servidor HTTP...
var server = require('http').createServer();
// El servidor web socket...
var ws = new require('ws').Server({
server: server,
});
// Hace que valga la pena vivir...
var express = require('express');
var app = express();
// Así podemos enviar "index.html"...
app.use(express.static(__dirname));
// Manejador para cuando un cliente se conecta via web socket.
ws.on('connection', function connection(ws) {
let i = 0;
const names = [ null, 'one', 'two', 'three' ];
// Envía al cliente 3 mensajes espaciados por
// intervalos de 1 segundo.
function interval() {
if (++i< 4) {
ws.send(JSON.stringify({
value: i,
task: names[i]
}));
setTimeout(interval, 1000);
}
}
setTimeout(interval, 1000);
});
// Pone en marcha los servidores HTTP y web socket.
server.on('request', app);
server.listen(8000, () => {
console.log('Listening on', server.address().port)
});
En el transcurso de tres segundos, veremos tres mensajes del web socket entregados al cliente. Cada
mensaje tiene una propiedad task, y este es el valor que estamos usando para determinar qué
action se envía. Echemos un vistazo al módulo frontend main.js y asegurémonos de que todo
funciona como se espera:
import dispatcher from './dispatcher';
import connect from './actions/socket-listener';
import { ONE } from './actions/one';
import { TWO } from './actions/two';
import { THREE } from './actions/three';

// Registra los mensajes de web socket que han sido


// enviados como actions Flux.
dispatcher.register((e) => {
switch (e.type) {
case ONE:
console.log('one', e.payload);
break;
case TWO:
console.log('two', e.payload);
break;
case THREE:
console.log('three', e.payload);
break;
}
});
// →
// one 1
// two 2
// three 3
// Establece la conexión web socket. Advierte
// que es importante conectar despues de que todo
// con el dispatcher Flux es configurado.
connect();
Como puedes ver, la función connect() es responsable de establecer la conexión del web socket.
Se trata de una implementación sencilla, que carece de varias funcionalidades de producción, como
la reconexión de conexiones caídas. Sin embargo, lo importante a tener en cuenta aquí es que este
listener se encuentra en realidad en el mismo directorio que los otros módulos de action. En realidad
queremos un fuerte acoplamiento porque el objetivo principal del listener de sockets es enviar
actions, traduciendo mensajes de web sockets.

Creadores de actions parametrizables


La sección final de este capítulo se centra en los creadores de actions parametrizados. Todas las
funciones de creación de actions que hemos visto hasta ahora en el capítulo han sido básicas que no
aceptan ningún argumento. Esto está bien, excepto cuando empezamos a acumular varias actions
únicas que son casi idénticas. Sin funciones de creación de action parametrizadas, pronto tendremos
una proliferación interminable de funciones; esto no escala.
Primero, estableceremos los objetivos de pasar argumentos a las funciones de creación de actions,
seguido de un código de ejemplo que implementa funciones genéricas de creación de actions. A
continuación, estudiaremos la creación de funciones parciales para reducir aún más la repetitividad
mediante la composición de creadores de action.

Eliminación de actions redundantes


Los creadores de actions son funciones JavaScript simples. Esto significa que pueden aceptar cero o
más argumentos cuando se le llama. El objetivo de implementar una función, independientemente
de si está o no en el contexto de Flux, es reducir la cantidad de código que tenemos que escribir. Es
probable que los creadores de actions en una aplicación Flux se acumulen porque impulsan el
comportamiento de nuestra aplicación. Si algo sucede, puede ser rastreado a una action. Así que es
fácil introducir varias actions nuevas a lo largo del curso de un día.
Una vez que nuestra aplicación tiene varias funcionalidades implementadas, estamos obligados a
tener un montón de actions. Algunas de estas actions tendrán una finalidad específica, mientras que
otras serán muy similares entre sí. En otras palabras, algunas actions empezarán a parecer
redundantes. El objetivo es eliminar las funciones redundantes del creador de actions mediante la
introducción de parámetros.
Debemos tener cuidado al refactorizar nuestra función creadora de actions. Hay un fuerte
argumento a favor de mantener una función dedicada para cada tipo de action en el sistema. Es
decir, una función creadora de actions debería sólo envíar un tipo de action, no una de varias
opciones. De lo contrario, el rastreo en nuestro código se verá disminuida. Deberíamos aspirar a
reducir el total número total de actions en el sistema.

Mantener las actions genéricas


Cuando las actions son genéricas, la arquitectura requiere menos de ellas. Esto es bueno porque
significa que hay menos conocimiento para mantener en nuestras cabezas mientras escribimos
código. Echemos un vistazo a un par de actions que esencialmente hacen lo mismo; en otras
palabras, no son genéricas en absoluto. La primera action es la siguiente:
import dispatcher from '../dispatcher';
import sortBy from 'lodash/sortBy';
// Identificadro de action...
export const FIRST = 'FIRST';
export function first() {
// Datos payload.
let payload = [ 20, 10, 30 ];
// Envía la action "FIRST" con
// el payload ordenado en sentido ascendente.
dispatcher.dispatch({
type: FIRST,
payload: sortBy(payload)
});
}
Bastante simple-es usar la función sortBy() para ordenar el payload antes de enviar la action.
Ten en cuenta que en realidad no clasificaríamos los datos de payload de esta manera en la
función de creación de actions. Piensa en esto como una simulación de API. El punto es
que la función creadora de la action está pidiendo algunos datos fuera de Flux.
Veamos otra action similar pero distinta:
import dispatcher from '../dispatcher';
import sortBy from 'lodash/sortBy';
// Identificador de action...
export const SECOND = 'SECOND';
export function second() {
// Los datos payload.
let payload = [ 20, 10, 30 ];
// Envía la action, con el
// payload ordenado en sentido descendente.
dispatcher.dispatch({
type: SECOND,
payload: sortBy(payload, x => x * -1)
});
}
La única diferencia aquí es cómo ordenamos los datos. Si se tratara de una función de creación de
actions de producción, le diríamos a la API que ordenara los datos en orden descendente.
¿Necesitamos dos actions distintas para los dos sentidos? ¿O podemos eliminarlos a ambos a favor
de un genérico que acepta un parámetro de dirección de ordenación? Aquí hay una implementación
genérica de la action:
import dispatcher from '../dispatcher';
import sortBy from 'lodash/sortBy';
// Identificador de action...
export const THIRD = 'THIRD';
// Acepta una dirección de ordenación, por defecto
// descenderá.
export function third(dir='desc') {
// Datos payload.
let payload = [ 20, 10, 30 ];
// La función iteradora que es pasada
// a "sortBy()".
let iteratee;
// Establecer el "iteratee" personalizado si
// queremos oredenar en sentido descendente.
if (dir === 'desc') {
iteratee = x => x * -1;
}
// Enviar la action, ordenando el payload
// basado en "dir".
dispatcher.dispatch({
type: THIRD,
payload: sortBy(payload, iteratee)
});
}
Aquí están las tres actions que se están usando. Advierta que la tercera action cubre ambos casos y,
sin embargo, la action de ordenación fundamental es la misma independientemente de los
argumentos que se le pasen. Puedes ver en la función de devolución de llamada del dispatcher que a
los stores les resultaría más fácil escuchar una action en lugar de dos o más:
import dispatcher from './dispatcher';
import { FIRST, first } from './actions/first';
import { SECOND, second } from './actions/second';
import { THIRD, third } from './actions/third';

// Registra los payloads específicos de action


// que han sido enviados.
dispatcher.register((e) => {
switch(e.type) {
case FIRST:
console.log('first', e.payload);
break;
case SECOND:
console.log('second', e.payload);
break;
case THIRD:
console.log('third', e.payload);
break;
}
});
first();
// → first [10, 20, 30]
second();
// → second [30, 20, 10]
third();
// → third [30, 20, 10]
third('asc');
// → third [10, 20, 30]

Creación de actions parciales


En algunos casos, los argumentos de la función son sencillos, ya que hay uno o dos de ellos. En
otros, las listas de argumentos pueden ser desalentadoras, especialmente cuando las llamamos
repetidamente usando el mismo puñado de argumentos. Los creadores de actions en aplicaciones
Flux no son diferentes. Habrá casos en los que tengamos una función genérica que soporte este
extraño caso, en lugar de una nueva función creadora de actions, simplemente proporcionamos un
parámetros diferente a la función. Pero en el caso más común, donde los mismos parámetros tienen
que ser suministrados todo el tiempo, esto puede llegar a ser repetitivo hasta el punto en que se
frustra el propósito de que tienen funciones genéricas.
Veamos una función genérica de creación de acciones que acepta un número variable de
argumentos. Puesto que los mismos argumentos se pasan a la función en el más, exportaremos una
versión parcial de la función donde estos argumentos se ha aplican parcialmente.
Los parámetros por defecto en la sintaxis ES2015 son una buena alternativa a la
creación de funciones parciales, pero sólo cuando el número de argumentos es fijo.
import dispatcher from '../dispatcher';
import partial from 'lodash/partial';
// Identificador de action...
export const FIRST = 'FIRST';
// Implementación genérica del creador de action.
export function first(...values) {
// Datos payload.
let defaults = [ 'a', 'b', 'c' ];
// Envío de la action "FIRST" con
// el array de “values” concatenado a
// el array “defaults”.
dispatcher.dispatch({
type: FIRST,
payload: defaults.concat(values)
});
}
// Exporta una versión común de "first()" con
// los argumentos cumunes ya aplicados.
export const firstCommon = partial(first, 'd', 'e', 'f');
Veamos como estas dos versiones del mismo creador de action son usadas:
import dispatcher from './dispatcher';
import { FIRST, first, firstCommon } from './actions/first';
// Registra los payloads de action como
// enviados.
dispatcher.register((e) => {
switch(e.type) {
case FIRST:
console.log('first', e.payload);
break;
}
});
// Llama al creador de action con un conjunto común
// de argumentos. Este es el tipo de código que
// queremos evitar repetir en todos los lugares.
first('d', 'e', 'f');
// → first ["a", "b", "c", "d", "e", "f"]
// La exacta misma cosa como la llamada "fist()" de ahí encima.
// Los argumentos comunes han sido parcialmente aplicados.
firstCommon();
// → first ["a", "b", "c", "d", "e", "f"]
Es importante advertir que las funciones first() y firstCommon() son el
mismo creador de actions, y por eso están definidas en el mismo módulo. Si
definiéramos firstCommon() en otro módulo de action, esto llevaría a confusión,
porque ambos usan el mismo tipo de action-FIRST.

Sumario
En este capítulo, aprendiste sobre las funciones de creación de actions que las aplicaciones de Flux
utilizan para enviar actions. Sin las funciones creadoras de action, tendríamos directamente la
interfaz con el dispatcher en nuestro código, lo que hace que la arquitectura menos razonable.
Empezamos pensando en las convenciones de nombres para la action y en las convenciones
generales de organización de nuestros módulos de action. Agrupar a los creadores de actions por
funcionalidades tiene implicaciones también para la modularidad, especialmente en la forma en que
ésta influye en la modularidad de otras áreas de la arquitectura.
A continuación, discutimos la simulación de datos usando funciones de creación de actions. Simular
los datos en las aplicaciones de Flux es fácil de hacer. Las actions son la única forma de que los
datos entren en el sistema, lo que nos facilita cambiar entre los datos de action simulados y nuestras
implementaciones de producción. Terminamos el capítulo con una mirada a los creadores de actions
con estado que escuchan cosas como conexiones de web socket, y una mirada a los creadores de
actions parametrizados que minimizan el código repetitivo.
En el próximo capítulo, trataremos otro aspecto clave de las funciones de los creadores de action-la
asincronía.
5
Actions asíncronas
En el Capítulo 4, Creando Actions, examinamos las actions de Flux en detalle – las funciones
creadoras de actions en particular. Un aspecto de las creadores de actions que no cubrimos fue el
comportamiento asincrónico. La asincronicidad es central para cualquier aplicación web, y en este
capítulo, vamos a pensar en lo que esto significa para una arquitectura Flux.
Comenzaremos cubriendo la naturaleza síncrona de Flux, como romper esta sincronicidad rompe
toda la arquitectura. A continuación, nos zambulliremos en algún código que haga llamadas a la API
y algunos creadores de actions que necesitan sincronizar múltiples llamadas a la API antes de
realmente enviar la action. Luego, presentaremos las promesas como valores de retorno de la
función creadora de action.

Manteniendo Flux sincrónico


Puede sonar extraño que queramos mantener una arquitectura sincrónica-especialmente en la web.
¿Qué hay de la lenta experiencia del usuario que ocurre cuando todo se realiza sincrónicamente?
Es sólo el flujo de datos Flux el que está sincronizado, no toda la aplicación. En este sección nos
referiremos a por qué mantener los mecanismos centrales de flujo de datos de nuestra arquitectura
sincrónizados es una buena idea. A continuación, hablaremos de cómo deberíamos encapsular el
comportamiento asíncrono en nuestra aplicación. Por último, vamos a repasar la sintaxis general de
cómo funcionan las funciones asíncronas del creador de actions.

¿Por qué sincronicidad?


La respuesta simple es que cualquier cosa que sea asíncrona introduce un nivel de incertidumbre
que de otra manera no existiría. Puede ser tentador, dado lo novedoso de los navegadores web,
hacer que todo suceda en paralelo, para aprovechar tantas solicitudes web simultáneas y tantos
núcleos de procesador como sea posible. Una vez que vamos por este camino, es difícil volver atrás,
y cuanto más abajo vamos, más enredada se vuelve la sintaxis de sincronización.
Pensemos por un momento en la API de DOM. Las aplicaciones JavaScript utilizan esta API para
cambiar el estado de los elementos de la página. Cuando se producen estos cambios, el motor de
renderizado del navegador se activa y realiza una actualización de la pantalla para que el usuario
pueda ver realmente los cambios. La API de DOM no interactúa directamente con lo que se muestra
en pantalla, hay un montón de detalles desagradables de los que el motor de renderizado se encarga
por nosotros. Esta idea se ilustra aquí:
Lo importante aquí es que no son las actualizaciones individuales hechas por nuestros componentes
que hacen que el motor de renderizado actualice la pantalla. El motor JavaScript se ejecuta hasta
completarse, lo que significa que espera a que todos estos componentes terminen de hacer sus
llamadas para actualizar el DOM (y cualquier otro código que estén ejecutando) antes de
desconectar el control al motor de renderizado. Esto significa que cualquier actualización que el
usuario vea es fundamentalmente síncrona - todo el código concurrente en el mundo no cambia el la
ruta de comunicación síncrona entre el motor de JavaScript y el motor de renderizado.
Puede ser que te preguntes qué tiene que ver esto con Flux en este punto. En realidad es muy
relevante porque los autores de Flux entienden esta actualización DOM sincróna así que en lugar de
combatirlo con código asíncrono complejo en todas partes, se les ocurrió una sintaxis de flujo de
datos que abraza la naturaleza sincróna de la actualización del DOM.
El núcleo de abstracción que Flux utiliza para el flujo de datos síncrono la ronda de actualización,
que se introdujo en el Capítulo 2, Principios de Flux. Nada puede interrumpir una ronda de la
actualización porque cada componente que participa en ella no tiene ningun comportamiento
asincróno. Si Flux tiene una tarea agotadora, es ésta. La ronda de actualización es una propiedad tan
crítica de las arquitecturas de Flux que tenemos que ser especialmente cuidadosos en su
mantenimiento. Es un amplio concepto-docenas de pequeños extremos causados por el
comportamiento asíncrono se quedan fuera de esto.

Encapsulación del comportamiento asíncrono


Con las rondas de actualización de Flux sincronizadas, ¿dónde deberíamos poner nuestro código
asíncrono? Pensemos en esto por un momento. A un lado de la arquitectura Flux, cualquier
comportamiento asíncrono va a actualizar el estado del sistema de alguna manera cuando la acción
se completa y se sincroniza con el resto de nuestro código. En algunas arquitecturas, esto sucede en
todas partes y no hay nada que lo proteja contra este tipo de acciones asíncronas de ser llamadas
desde lugares donde no se debería.
Por ejemplo, una ronda de actualización de Flux nunca debe dar lugar a la ejecución de un nuevo
comportamiento. Sabemos que las rondas de actualización son síncronas, así que esto no es un
comienzo. Nosotros tenemos que encapsular nuestro comportamiento asíncrono de alguna manera.
En esto es lo que las funciones de creación de actions son realmente buenas, para realizar el trabajo
asíncrono y gestionar los envíos de la acción una vez que la parte asíncrona se ha completado. Aquí
esta una visualización de las funciones de creación de actions que encapsulan llamadas asíncronas:
Mantener un comportamiento asíncrono en las funciones de creación de acciones hace dos cosas por
nosotros. Primero, sabemos que no hay de sincronización involucrada en llamar al creador de la
acción - todo esto se maneja dentro de la función para nosotros. La segunda ventaja es que todo
nuestro comportamiento asíncrono se puede encontrar dentro de una sola capa arquitectónica. Es
decir, si hay algo que es asíncrono, como hacer una llamada a la API, sabemos dónde buscar este
código.

de acción asíncrona
Depende de nuestras funciones de creación de acciones el realizar cualquier sincronización antes de
enviar cualquier acción. Hay dos partes en una función de creación de acción determinada. En
primer lugar, las llamadas asíncronas, si las hay, mientras que la segunda parte es la planificación
real de la acción. El trabajo de estas funciones de creación de acciones es sincronizar la llamada
async con el dispatcher de Flux, lo que significa que la función tendrá que esperar algún tipo de
respuesta antes de que la acción pueda ser enviada.
Esto se debe a que la acción asíncrona tiene datos payload. Echemos un vistazo a un ejemplo, ¿sí?
He aquí una función de creación de acciones que llama a una API para cargar una lista de objetos
usuario:
import dispatcher from '../dispatcher';
// Identificador de la acción...
export const LOAD_USERS = 'LOAD_USERS';
// Interpreta cierto comportamiento asíncrono, y una vez
// completa, envía la acción.
export function loadUsers() {
// Crea una nueva promesa, destinada a simular
// una llamada a alguna función de la API, la cual probablemente
// también devolvería una promesa.
let api = new Promise((resolve, reject) => {
// Resuelve la promesa con algún dato después de medio
// segundo.
setTimeout(() => {
resolve([
{ id: 1, name: 'User 1' },
{ id: 2, name: 'User 2' },
{ id: 3, name: 'User 3' }
]);
}, 500);
});
// Cuando la promesa resuelve, el callback que es
// pasado al "then()" es llamado con los valores
// devueltos. Este es el payload enviado.
api.then((response) => {
dispatcher.dispatch({
type: LOAD_USERS,
payload: response
});
});
}
Como puedes ver, estamos usando una promesa en lugar de una llamada real a la API. En términos
generales, nuestra aplicación probablemente tendrá una llamada a una función de la API que
devuelve una promesa. Esto es exactamente lo que estamos haciendo aquí, hacer que parezca que
estamos hablando con una API cuando en realidad es sólo una promesa. La mecánica es la misma,
independientemente de si es setTimeout() o una respuesta AJAX real la que devuelve la
promesa.
Lo importante a tener en cuenta es que es la función loadUsers() la que se encarga de enviar la
acción después de que la promesa se ha resuelto. Piénsalo de esta manera: nunca se invoca al
dispatcher a menos que tengamos nuevos datos para el sistema. La espera queda fuera de la ronda
de actualización de Flux, por lo que es bueno mantener todo junto en una función como esta. Así es
como usamos la función loadUsers():
import dispatcher from './dispatcher';
import {
LOAD_USERS,
loadUsers
} from './actions/load-users';
// Registra los payloads específicos de la acción como
// si hubieran sido enviados.
dispatcher.register((e) => {
switch(e.type) {
case LOAD_USERS:
console.log('users', e.payload.map(x =>x.id));
break;
}
});
loadUsers();
// → users [1, 2, 3]
Algo que puede haber notado que falta en este ejemplo es cualquier tipo de manejo
de errores. Por ejemplo, sería desagradable llamar a loadUsers() y que falle
silenciosamente porque algo anda mal con la API. En la sección final de este capítulo
trataremos más a fondo la gestión de errores.
Realización de llamadas a API
En esta sección, repasaremos el caso común del comportamiento asíncrono en las arquitecturas Flux
- hacer llamadas API a través de la red. A continuación, discutiremos algunas de las implicaciones
del comportamiento asíncrono en el contexto de la interactividad del usuario y las herramientas de
Flux disponibles para tratarlas.

Las APIs son el caso común


La arquitectura Flux es para el frontend de las aplicaciones web. Dicho esto, va a haber mucha
comunicación en red entre algunos componentes de nuestra arquitectura y la API de backend. Este
es un caso común de comportamiento asíncrono, no sólo en Flux, sino en la mayoría de las
aplicaciones JavaScript. Por lo tanto, aquí es donde el énfasis debería estar cuando se diseñan
creadores de acción que se comunican directamente de forma asincrónica con estos endpoints de la
API. A continuación, se muestran las rutas de comunicación más comunes en aplicaciones Flux:

Las stores deben estar llenas de datos, y la forma más común de obtenerlos es obteniéndolos de la
API. De hecho, es probable que el usuario vaya a gastar más tiempo consumiendo información que
interactuando con los elementos de la interfaz de usuario. Como viste en la última sección, la
sincronización de la respuesta con el dispatcher no es difícil de hacer con promesas.
Estos tipos de llamadas a API no son la única fuente de datos asíncronos en arquitecturas Flux. Por
ejemplo, la lectura de un fichero utilizando la API requiere el uso de una función asíncrona.
Interactuar con los workers de la web es otra forma de comunicación asíncrona-le pides al worker
que calcule algo y obtenga una respuesta en forma de una función callback. Aunque menos
comunes que las llamadas HTTP, estas interfaces asíncronas se pueden tratarse del mismo modo,
como se ilustra aquí:

El mismo mecanismo de sincronización (promesas) puede utilizarse para todos estos tipos de
canales de comunicación asíncronos. En cuanto a las funciones de creación de acciones, todas
tienen la misma interfaz, un valor prometido que se resuelve posteriormente. La sintaxis del
dispatcher es la misma aquí también.
No hay ningún comportamiento asíncrono que entre en la ronda de actualización de Flux porque
todo está encapsulado dentro de las propias funciones del creador de acciones. Además, podría ser
necesaria más de una API para obtener todos los datos necesarios para obtener un payload de
acción. Lo veremos en breve. Por ahora, volvamos nuestra atención a cómo los creadores de
acciones asíncronas impactan en la interactividad del usuario.
Llamadas a la API e interactividad con el usuario
El principal reto con las llamadas asíncronas y los elementos de la interfaz de usuario es que tienen
que gestionar el estado de la solicitud, que a su vez refleja el estado de los elementos de la interfaz
de usuario. Por ejemplo, cuando el usuario envía un formulario, tenemos que dar alguna clase de
indicación visual de que la solicitud ha sido hecha y que está siendo procesada. Además, también
necesitamos evitar que el usuario interactúe con ciertos elementos de la interfaz de usuario hasta
que vuelva una respuesta con el estado de la solicitud.
Las stores en una arquitectura Flux contienen todos los estados de la aplicación, incluyendo el
estado de cualquier petición de red que queramos realizar. Esto puede ayudarnos a coordinar el
estado de elementos relevantes de la interfaz de usuario con una solicitud determinada. Veamos un
creador de acción que envía una petición de API asíncrona para iniciar algo:
import dispatcher from '../dispatcher';
// Identificador de la acción...
export const START = 'START';
export function start() {
// Simula una llamada async a una API que inicia
// algo. La promesa resuelve después de
// un segundo.
let api = new Promise((resolve, reject) => {
setTimeout(resolve, 1000);
});
// Envía la acción despues de que la promesa
// a sido resuelta.
api.then((response) => {
dispatcher.dispatch({ type: START });
});
}
Como puede ver, la función start() envía la acción START después de que la promesa se
resuelve. Al igual que una llamada real a la API, este retraso permite al usuario disponer de tiempo
suficiente para interactuar con la interfaz de usuario antes de que vuelva la llamada. Por lo tanto,
tenemos que tomar medidas para evitar que esto ocurra. Veamos otra función creadora de acciones
que le informa al sistema sobre el estado de la solicitud de API que acabamos de hacer:
import dispatcher from '../dispatcher';
export const STARTING = 'STARTING';
export function starting() {
dispatcher.dispatch({ type: STARTING });
}
Al llamar a starting(), podemos informar a cualquier store que esté escuchando que estamos a
punto de hacer una llamada a la API para iniciar algo. Esto podría ser lo que necesitamos para
encargarnos de manejar el estado de los elementos de la interfaz de usuario para informar al usuario
de que la solicitud está en curso, y para desactivar los elementos que el usuario no debe tocar
mientras se realiza la solicitud. Echemos un vistazo a una tienda que procesa este tipo de acciones.
La store también procesa acciones de STOP y STOPPING. Estos módulos no se listan
por aquí porque son casi idénticos a las acciones START y STARTING,
respectivamente.
import dispatcher from '../dispatcher';
import {
START,
STARTING,
STOP,
STOPPING
} from '../actions/constants';
import { EventEmitter } from 'events';
class MyStore extends EventEmitter {
constructor() {
super();
this.state = {
startDisabled: false,
stopDisabled: true
};
dispatcher.register((e) => {
switch(e.type) {
// Si iniciamos o detenemos, no queremos que ningún
// botón esté disponible.
case STARTING:
case STOPPING:
this.state.startDisabled = true;
this.state.stopDisabled = true;
this.emit('change', this.state);
break;
// Deshabilitar el botón stop tras haber comenzado.
case START:
this.state.startDisabled = true;
this.state.stopDisabled = false;
this.emit('change', this.state);
break;
// Deshabilitar el botón start cuando haya sido detenido.
case STOP:
this.state.startDisabled = false;
this.state.stopDisabled = true;
this.emit('change', this.state);
break;
}
});
}
}
export default new MyStore();
El store tiene una representación clara del estado desactivado tanto para el botón start como para
el de stop. Si la acción STARTING o STOPPING es enviada, entonces podemos marcar ambos
botones como deshabilitados. En el caso de START o STOP, podemos marcar el botón
correspondiente como desactivado y el otro como activado. Ahora que las stores tienen todos los
estados que necesitan, veamos una vista que realmente muestre los elementos de los botones.
Puede que te preguntes por qué hemos separado estas dos acciones en dos funciones
de creador de acciones: start() y starting(). La razón es simple: un creador
de acción envía una acción. Sin embargo, esto no está grabado en piedra y es una
cuestión de preferencia personal. Por ejemplo, start() podría haber enviado la
acción STARTING antes de que realmente haga la llamada a la API. Lo bueno es
que sólo hay una función que se encarga de todo. En el lado negativo, perdemos el
uno a uno correspondiente entre el creador de la acción y la acción, aumentando la
confusión.
import myStore from '../stores/mystore';
import {
start,
starting,
stop,
stopping
} from '../actions/functions';
class MyView {
constructor() {
// Los elementos de nuestra vista con los que interactuar....
this.start = document.getElementById('start');
this.stop = document.getElementById('stop');
// El botón start fue pulsado. Envia la
// acción "STARTING", y la acción "START"
// una vez la llamada asíncrona se resuelve.
this.start.addEventListener('click', (e) => {
starting();
start();
});
// El botón stop fue pulsado. Envía la
// acción "STOPPING", y la acción "STOP"
// una vez la llamada asíncrona se resuelve.
this.stop.addEventListener('click', (e) => {
stopping();
stop();
});
// Cuando el estado de la store cambia, modifica la UI
// habilitando o deshabilitando los botones,
// dependiendo del estado de la store.
myStore.on('change', (state) => {
this.start.disabled = state.startDisabled;
this.stop.disabled = state.stopDisabled;
});
}
}
export default new MyView();
Tenga en cuenta que el trabajo principal de los manejadores de clics es llamar a las funciones de
creación de acciones. No realizan comprobaciones de estado adicionales para asegurarse de que se
pueden llamar las acciones. Este tipo de cosas no pertenecen a las vistas, sino a una store. Estamos
siguiendo esta táctica aquí, donde desactivamos los botones en la store cambiando un estado
concreto. Si comprobamos este tipo de cosas a la vista de los manejadores de eventos, acabamos
desacoplando el estado desde la lógica que opera sobre él, y en Flux esto no es algo bueno.

Combinando llamadas API


A medida que el desarrollo avanza y las funciones se vuelven más complejas, inevitablemente nos
enfrentamos a complejos escenarios de API. Esto significa que ya no existe un simple endpoint de
API que ofrezca todo lo que la función necesita con una sola llamada. En su lugar, nuestro código
tiene que unir dos o más recursos de diferentes endpoints sólo para obtener los datos necesarios para
la función.
En esta sección, veremos las funciones de creación de acciones que obtienen datos de múltiples
recursos asíncronos y los pasan a las stores como datos payload. Estas stores los convierten en
información requerida por las funcionalidades. Luego, veremos un enfoque alternativo, donde
componemos funciones creadoras de acción a partir de creadores de acción más pequeños. cada uno
extrayendo datos de su propio recurso asíncrono.

Creadores de acciones complejas


A veces, un único endpoint de la API no tiene todos los datos que necesitamos para una determinada
store. Esto significa que tenemos que obtener datos de múltiples endpoints de la API. El reto es que
se tratan de recursos asincrónos, y que necesitan ser sincronizados. antes de pasarlos a las stores
enviándolos como payloads de acción. Tomemos un momento para ver un creador de acciones que
obtiene datos de tres endpoints de API asíncronos. Pero primero, aquí están las funciones de la API
que usaremos para simular llamadas de red asíncronas:
// Función auxiliar de API – devuelve los
// “datos” después de unos MS de “retraso”.
function api(data, delay=1000) {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve(data);
}, delay);
});
}
// Primera API...
export function first() {
return api([ 'A', 'B', 'C' ], 500);
}
// Segunda API...
export function second() {
return api([ 1, 2, 3 ]);
}
// Tercera API...
export function third() {
return api([ 'D', 'E', 'F' ], 1200);
}
Así que tenemos valores de retorno consistentes de estas promesas de funciones de API. Cada
promesa que se devuelve desde una función dada es responsable de sincronizar esa llamada de API.
¿Pero qué pasa cuando nuestra tienda necesita combinar todos estos valores resueltos para formar el
estado de una store? Veamos ahora una función creadora de acciones que se encarga de esto:
import dispatcher from '../dispatcher';
// Funciones API simuladas que necesitamos.
import {
first,
second,
third
} from './api';
// Identificador de acción...
export const MY_ACTION = 'MY_ACTION';
export function myAction() {
// Llama a las tres APIs, las cuales resuelven
// tras restrasos distíntos. El método
// "Promise.all()" las sincroniza y devuelve una nueva promesa.
Promise.all([
first(),
second(),
third()
]).then((values) => {
// Valores devueltos...
let [ first, second, third ] = values;
// Las tres llamadas API han sido resueltas, significa que
// podemos enviar "MY_ACTION" con los tres
// valores async como el payload.
dispatcher.dispatch({
type: MY_ACTION,
payload: {
first: first,
second: second,
third, third
}
});
});
}
La acción MY_ACTION sólo se envía cuando los tres valores asincrónicos están devueltos, porque
la store depende de los tres. Los tres valores están disponibles para el store en un solo ciclo de
actualización cuando se envía la acción. Algo menos obvio acerca de este código, pero importante
sin embargo, es el hecho de que no estamos realizando ninguna transformación de datos dentro de
la función creadora de acciones antes de enviar el payload. En su lugar, proporcionamos los datos
devueltos de la API tal cual, en el formulario como propiedades payload. Esto asegura que la store
sea la única responsable de el estado de su información. Veamos cómo una store es ahora capaz de
usar este payload:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { MY_ACTION } from '../actions/myaction';
class MyStore extends EventEmitter {
constructor() {
super();
this.state = [];
dispatcher.register((e) => {
switch(e.type) {
case MY_ACTION:
// Obtiene los valores async devueltos del
// payload de la acción.
let { first, second, third } = e.payload;
// Comprime los tres arrays y asigna el array
// resultado como estado de la store.
this.state = first.map((item, i) =>
[ item, second[i], third[i] ]);
this.emit('change', this.state);
break;
}
});
}
}
export default new MyStore();
Como puedes ver, la store tiene todo lo que necesita en el payload para realizar las transformaciones
necesarias. Llamemos a la función creadora de acción y veamos si esta store se comporta como se
espera:
import { myAction } from './actions/myaction';
import myStore from './stores/mystore';
myStore.on('change', (state) => {
console.log('changed', state);
});
myAction();
// → changed
// [
//[ 'A', 1, 'D' ],
//[ 'B', 2, 'E' ],
//[ 'C', 3, 'F' ]
// ]

Componer creadores de acciones


Como has visto antes en el capítulo, nuestras llamadas de función creadoras de acción pueden ser
bastante verbosas cuando se trata de interactividad con el usuario. Esto es porque tenemos que hacer
dos o tres llamadas más a funciones de creación de acción. Una llamada asegura de que los
elementos de la UI están en el estado que es apropiado mientras el usuario espera que se complete la
acción asíncrona. La otra llamada invoca el comportamiento asíncrono. Para evitar tener que hacer
dos llamadas en todas partes, podríamos hacer que la función creadora de acciones enviara dos
acciones. Sin embargo, esto no siempre es lo ideal porque podríamos necesitar llamar a la primera
accíón del creador sin necesidad de llamar a la segunda acción en algún momento. Es un problema
de escalabilidad más que otra cosa.
La solución fácil es componer una función de las dos. De esta manera, mantenemos la escalabilidad
intacta, a la vez que reducimos el número de funciones a llamar en muchos lugares. Volvamos a
nuestro código de antes, donde tuvimos que llamar manualmente a starting() y luego a
start():
import { start as _start } from ‘./start’;
import { starting } from ‘./starting’;
import { stop as _stop } from ‘./stop’;
import { stopping } from ‘./stopping’;

// La función "start()" llama automáticamente


// "starting()" ahora.
export function start() {
starting();
_start();
}
// La función "stop()" llama automticamente
// "stopping()" ahora.
export function stop() {
stopping();
_stop();
}
// Exporta "starting()" y "stopping()" así
// que pueden seguir siendo usados por sí
// mismos, o componer otras funciones.
export { starting, stopping };
Ahora nuestras vistas pueden simplemente llamar a start() o stop() y los cambios de estado
necesarios se aplican a los elementos relevantes de la interfaz de usuario. Esto funciona porque la
primera función de creación de acciones es síncrona, lo que significa que la ronda completa de
actualización de Flux tiene lugar antes de que se realice la llamada asíncrona. Este comportamiento
es consistente, pase lo que pase. Donde empezamos a tener problemas es cuando empezamos a
componer funciones a partir de varios creadores de acciones asíncronas, como se visualiza aquí:

El problema aquí es que cada una de estas funciones de asyncAction() que hemos usado para
componer action() resulta en una ronda de actualización. La ronda de actualización que ocurre
primero es una condición de carrera. No podemos combinarlas en un solo creador de acción que
haga peticiones a múltiples endpoints de la API porque dan servicio a dos stores diferentes. En Flux
es todo sobre flujos de datos predecibles, y esto significa conocer siempre el orden de las rondas de
actualización. En la siguiente sección, revisaremos las promesas en las funciones creadoras de
acciónes para ayudarnos a sortear estos complicados escenarios de creadores de acción asíncronos.

Devolviendo promesas
Ninguna de las funciones de creación de acciones que hemos visto hasta ahora en este capítulo
devolvió ningún valor. Esto se debe a que su trabajo principal es enviar las acciones, mientras que al
mismo tiempo ocultan cualquier sintaxis de sincronización de concurrencia. Por otro lado, las
funciones creadoras de acción podrían devolver una promesa para que pudiéramos componer más
comportamientos asincronos complejos que se extiendan a varias stores. En la última sección,
nosotros vimos que componer comportamiento asíncrono usando funciones creadoras de acciones
puede ser difícil, si no imposible de hacer.
En esta sección, revisaremos los desafíos que plantea el comportamiento asíncrono en el contexto
de la composición de una mayor funcionalidad. A continuación, crearemos una implementación de
ejemplo con creadores de acciones que devuelven promesas y las utilizan para sincronizar con otras.
Finalmente, veremos si devolver las promesas de los creadores de acciones puede ayudarnos a lidiar
con los errores que ocurren en los recursos asíncronos con los que nos comunicamos.

Sincronizar sin promesas


Un aspecto agradable de una arquitectura Flux es el hecho de que gran parte de ella es síncrona. Por
ejemplo, cuando llamamos al dispatcher con una nueva acción y un nuevo payload, podemos estar
seguros de que la llamada se bloqueará hasta que la ronda de actualización haya terminado, y se
refleje el estado actual en la interfaz de usuario. Con el comportamiento asíncrono, las cosas son
diferentes-especialmente en una arquitectura Flux donde este tipo se limita estrictamente a las
funciones creadoras de acción. Por lo tanto, nos enfrentamos al inevitable desafío de tratar de armar
sistemas complejos a partir de una abundancia de recursos asíncronos.
Vimos cómo llegar hasta allí antes en el capítulo. Un único creador de acciones puede combinar los
valores resueltos de varios recursos asíncronos en una sola acción y un payload. Entonces la lógica
dentro de la store puede averiguar cómo utilizar los datos y actualizar su estado. Esto funciona bien
cuando las stores son individuales pero se tambalea cuando intentamos sincronizar recursos en
varias stores y funcionalidades.
Esto es cuando el poder sincronizar los datos asíncronos y la ronda de actualización llega a ser
importante. Para hacerlo, nuestras funciones creadoras de acciones necesitan devolver promesas que
se resuelven cuando ambas se han completado. He aquí una ilustración de lo que necesitamos
lograr:

Componiendo comportamientos asíncronos


La forma de sortear estos complicados escenarios de creadores de acciones asíncronas es que estas
funciones devuelvan promesas que se resuelven después de que el comportamiento asíncrono y la
ronda de actualización se hayan completado. Esto le permite a la persona que llama saber que la
ronda de actualización está completa y que cualquier cosa que llamemos ahora tendrá lugar
después. Lo que buscamos aquí es coherencia, así que echemos un vistazo a una función creadora
de acciones que devuelve una promesa:
// Identificador de acción...
export const FIRST = 'FIRST';
// Función API que devuelve una promesa que es
// resuelta tras 1.5 segundos.
function api() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ first: 'First Name' });
}, 1500);
});
}
export function first() {
// Devuelve una promesa así que el llamador
// conozca cuando la ronda de actualización está completa,
// independientemente del comportamiento asíncrono
// que tiene lugar antes de que la acción sea enviada.
return new Promise((resolve, reject) => {
api().then((response) => {
// La acción es enviada despues de que el valor
// asíncrono sea devuelto.
dispatcher.dispatch({
type: FIRST,
payload: response
});
// Resuelve la promesa devuelta por "first()",
// después de la ronda de actualización.
resolve();
});
});
}
Por lo tanto, este creador de acciones llama a una API asíncrona que se resuelve después de 1,5
segundos, momento en el que se envía el payload de la acción y se resuelve la promesa devuelta.
Echemos un vistazo a otro creador de acciones que utiliza una función de API diferente:
import dispatcher from '../dispatcher';
// Identificador de acción...
export const LAST = 'LAST';
// La función API que devuelve una promesa que se
// resolverá tras 1.5 segundos.
function api() {
return new Promise((resolve, reject) => {
setTimeout(() => {
resolve({ last: 'Last Name' });
}, 1000);
});
}
export function last() {
return new Promise((resolve, reject) => {
api().then((response) => {
dispatcher.dispatch({
type: LAST,
payload: response
});
resolve();
});
});
}
Puedes ver que los dos creadores de acción -first() y last()- siguen una estrategia idéntica
devolviendo promesas. La función API, sin embargo, resuelve diferentes datos, y sólo tarda 1
segundo en hacerlo. Veamos qué sucede cuando intentamos usar estas dos funciones juntas:
import dispatcher from './dispatcher';
import { FIRST, first } from './actions/first';
import { LAST, last } from './actions/last';
// Registra los payload como acciones enviadas...
dispatcher.register((e) => {
switch (e.type) {
case FIRST:
console.log('first', e.payload.first);
break;
case LAST:
console.log('last', e.payload.last);
break;
}
});
// El orden de la ronda de actualización no está garantizada aquí.
first();
last();
// →
// last Last Name
// first First Name
// Con promesas, el orden de la ronda de actualizción es consistente.
first().then(last);
// →
// first First Name
// last Last Name

Manejo de errores
¿Qué sucede cuando falla la API con la que interactúan los creadores de acciones de Flux? En
general, cuando hacemos llamadas AJAX, proporcionamos finciones tanto de llamadas de éxito
como de error. De esta manera, podemos fallar de una manera elegante. Tenemos que tener cuidado
con cómo manejamos los errores en los creadores de acciones de Flux porque, al igual que las stores
quieren saber sobre acciones, también quieren saber sobre fallos.
Así que la pregunta es: ¿qué hacemos diferente en nuestras funciones de creador de acciones?
¿Simplemente enviamos algún tipo de acción de error desde el creador de la acción cuando la API
falla? Queremos enviar una acción de error para que las stores puedan ajustar su estado. en
consecuencia, ¿pero qué hay de la persona que llama al creador de la acción? Por ejemplo,
podríamos tener una función genérica de creación de acciones que se usa en muchos lugares, y el
manejo del error podría depender del contexto.
La respuesta es rechazar la promesa que es devuelta por el creador de la acción. Esto permite, a la
persona que llama, especificar su propio comportamiento en el caso de una llamada de API fallida.
Vamos ver una función del creador de acciones que maneja los errores de esta manera:
import dispatcher from '../dispatcher';
// Identificador de acción...
export const UPDATE_TASK = 'UPDATE_TASK';
// Identificación de acción fallida...
export const UPDATE_TASK_ERROR = 'UPDATE_TASK_ERROR';
// Deuelve una promesa que es rechazada con un mensaje
// de error tras 0.5 segundos.
function api() {
return new Promise((resolve, reject) => {
setTimeout(() => {
reject('Failed to update task');
}, 500);
});
}
export function updateTask() {
return new Promise((resolve, reject) => {
// Envía la acción "UPDATE_TASK" como es natural
// cuando la promesa es resuelta. Entonces resuelve
// la promesa devuelta por "updateTask()".
api().then((response) => {
dispatcher.dispatch({
type: UPDATE_TASK
});
resolve();
// Si la promesa de la API falla, rechaza la promesa
// devuelta por "updateTask()" también.
}, (error) => {
reject(error);
});
});
}
// Básico creador de acciones auxiliar para cuando
// la función "updateTask()" es rechazada.
export function updateTaskError(error) {
dispatcher.dispatch({
type: UPDATE_TASK_ERROR,
payload: error
});
}
Ahora llamemos esta función updateTask() y veamos si podemos asignar el comportamiento
con el manejo de errores a ella:
import dispatcher from './dispatcher';
import {
UPDATE_TASK,
UPDATE_TASK_ERROR,
updateTask,
updateTaskError
} from './actions/update-task';
// Registra el payload como acción enviada...
dispatcher.register((e) => {
switch (e.type) {
case UPDATE_TASK:
console.log('task updated');
break;
case UPDATE_TASK_ERROR:
console.error(e.payload);
break;
}
});
// Podemos decirle a "updateTask()" como responder cuando
// cuando la llamada subyacente a la API falle.
updateTask().catch(updateTaskError);
// → Failed to update task

Sumario
Este capítulo se centró en los creadores de acciones asíncronas en arquitecturas Flux. Estas son
funciones que necesitan planificar acciones, pero antes de que puedan, tienen que esperar a que
algún recurso asincrónico se resuelva. Miramos la ronda de actualización síncróa que es central en
cualquier arquitectura Flux. Luego, discutimos cómo los creadores de acciones encapsulan el
comportamiento asíncrono de tal manera que preservan las rondas de actualización sincronizadas.
Las llamadas de a recursos externos son la forma más común de comunicación asíncrona en las
aplicaciones JavaScript, incluyendo las arquitecturas Flux. Cubrimos la diferencia entre estos y
otros canales asíncronos, y cómo se pueden utilizar las promesas para cerrar la brecha entre ellos.
También examinamos la forma en que las promesas pueden ser utilizadas por los creadores de
acciones para permitir la composición de funcionalidades más complejas.
En el próximo capítulo, echaremos un vistazo más profundo a las stores y todo lo que tienen que
hacer para mantener un estado consistente en nuestras arquitecturas Flux.
6
Cambiando el estado de la store en Flux
Este capítulo trata sobre la continua evolución de nuestras stores Flux, como las funcionalidades de
la aplicación que implementamos impulsan mejoras arquitectónicas. De hecho, esto es algo en lo
que sobresalen las arquitecturas de Flujo por adaptarse a los cambios influenciados según van
sucediendo en la aplicación. Este capítulo se adentra en el cambio de diseño de stores y reforzar la
idea de que las stores cambiarán a menudo. Los cambios de mayor nivel en nuestras stores podrían
ser necesarios, como la introducción de stores genéricas que son compartidas por otras stores que se
centran en funcionalidades específicas. A medida que las stores evolucionan, también lo hacen las
dependencias entre ellas; veremos cómo gestionar las dependencias entre stores utilizando el
dispatcher. Cerraremos el capítulo con una discusión sobre cómo mantener a raya la complejidad de
las stores.

Adaptación a la información cambiante


Anteriormente en el libro, mencioné que las stores no son modelos de arquitecturas MV*. Son
diferentes desde varios puntos de vista, incluyendo su capacidad para hacer frente a modificar
esquemas en otras áreas arquitectónicas, como la API y los cambios en sus requisitos. En esta
sección, veremos la capacidad del store Flux para adaptarse a los cambios de las APIs. También
abordaremos la dirección opuesta del cambio, cuando las vistas que consumen los datos de store
tienen necesidades cambiantes. Por último, hablaremos de otros componentes que podría cambiar
como resultado directo de la evolución continua de una store.

Cambiar los datos de la API


Los datos de la API cambian, especialmente durante las primeras etapas de desarrollo. Aunque nos
decimos a nosotros mismos que una API dada se va a estabilizar con el tiempo, esto rara vez
funciona en la práctica. O si una API se vuelve estable e invariable, terminamos teniendo que usar
una API diferente. La suposición segura es que estos datos van a cambiar, y nuestras stores tendrán
que adaptarse a dichos cambios.
Lo bonito de los stores de Flux es el hecho de que son dirigidos más por las funcionalidades que por
la API. Esto significa que los cambios en los datos de la API tienen menos impacto en las stores
porque su trabajo es transformar los datos de la API en información requerida por la funcionalidad.
He aquí una visualización de esta idea:

A diferencia de los modelos, no estamos intentando representar los datos de la API en las stores tal
como están. Las stores se aferran al estado que sirve como información consumida por las
funcionalidades que usan nuestros clientes. Esto significa que cuando se producen cambios en los
datos de la API de los que depende una store determinada, tenemos que volver a visitar las
funciones de transformación que crean la información. Por otro lado, los modelos que son utilizados
por muchas vistas diferentes en muchas funcionalidades diferentes de la aplicación tienen más
dificultades para hacer frente a cambios en la API como estos. Es porque estos componentes tienen
dependencias con el esquema de los datos de la API, y no con el estado real que es relevante para
los elementos de la interfaz de usuario que tenemos que representar.
¿Podemos recrear siempre la información que se utiliza en nuestra arquitectura después de que se ha
producido un cambio en la API? No siempre. Y esto requiere que revisemos cómo nuestras vistas
interactúan con la store. Por ejemplo, si las propiedades se eliminan por completo de un esquema de
API dado, esto probablemente requerirá más que una simple transformación en nuestra store. Pero
este es un caso raro; el caso más común es que la store Flux puede adaptarse fácilmente a los
cambios en los datos de la API.

Modificar las características de una funcionalidad


Las stores cambian y evolucionan a través de los cambios en los datos de la API. Esto puede afectar
a la información disponible para las funciones que dependen de la store. A medida que nuestra
aplicación crece, las stores pueden sentir presión en la dirección opuesta: cambiar las características
de una funcionalidad a menudo conlleva nueva información. Este concepto se ilustra en el siguiente
diagrama:

En lugar de que los datos de la API dicten por sí solos lo que sucede en la función transform(),
es al revés. La funcionalidad y la información que la impulsa sirven para transformar el diseño de la
store. Esto puede ser más difícil que adaptarse a los cambios en los datos de la API. Hay dos
razones principales.
Primero, está la información en sí misma. La tienda puede transformar recursos en lo que la
funcionalidad necesita. Pero las tiendas no son mágicas: los datos que la API debe proporcionar
deben cubrir al menos las necesidades básicas en términos de datos; de lo contrario, estamos en un
callejón sin salida. En segundo lugar, son los propios elementos de la interfaz de usuario, algunos de
los cuales tienen estados que es necesario capturar por la store. La combinación de estos dos
factores puede suponer un reto.
Es bueno obtener respuestas a estas difíciles preguntas relacionadas con las funciones sobre la
información más pronto que tarde. Ser capaces de trabajar en esta dirección significa que estamos
dejando que la información que los usuarios necesitan dirija el diseño, en lugar de dejar que la API
disponible sea el que lo dirija.

Componentes impactados
Como vimos anteriormente en esta sección, las tiendas transforman sus fuentes de datos en
información que es consumible por el usuario. Esta es una gran característica arquitectónica de
Flux, porque significa que las vistas que escuchan estas tiendas no están teniendo constantemente
que cambiar como resultado de los cambios realizados en la API. Sin embargo, necesitamos ser
conscientes del impacto a otros componentes cuando las stores evolucionan.
Pensemos por un momento en las acciones. Cuando cambian los datos de la API, ¿es probable que
esto resulte en nuevas acciones que necesitamos enviar? No, porque es probable que estemos
tratando con puntos de entrada existentes en el sistema: estas acciones ya existen. ¿Qué hay de la
funcionalidad? ¿se traduce en nuevas acciones? Esto es probable, porque podríamos ver una nueva
interactividad del usuario lanzada por una funcionalidad o nuevos datos. Los payloads de una
acción existente también pueden evolucionar en respuesta a los cambios en los elementos de la
interfaz de usuario, por ejemplo.
Otra cosa a considerar es el efecto que un cambio en una store tiene en otras stores que dependen de
ella. ¿Seguirán siendo capaces de obtener la información que necesitan después del cambio? Las
vistas no son los únicos componentes de Flux que tienen dependencias con las stores. Entraremos
en las dependencias entre tiendas en mayor profundidad más adelante en el capítulo.

Reducir los datos duplicados en store


Las stores nos ayudan a dividir los estados que se encuentran en nuestras arquitecturas en
funcionalidades. Esto funciona bien porque podemos tener estructuras de datos drásticamente
diferentes de una funcionalidad a otra. Alternativamente, podríamos encontrar que, a medida que se
introducen nuevas funcionalidades, muchos de los mismos datos comienzan a aparecer en diferentes
stores. Nadie quiere repetirse, es ineficiente y podemos hacerlo mejor.
En esta sección presentaremos la noción de stores genéricas. Estos tipos de stores no son utilizados
necesariamente por las vistas, sino por otras stores como una especie de repositorio de datos
comunes. Luego repasaremos la configuración básica de una store genérica y cómo podemos poner
en uso las stores genéricas en nuestras stores más especializadas.

Datos de store generica


Las stores genéricas son similares a las clases superiores en una jerarquía de clases. Una clase padre
tiene el comportamiento y las propiedades comunes que se encuentran en algunos hijos. Sin
embargo, a diferencia de las jerarquías de clases, no tenemos varios niveles de estructura. El
objetivo de las stores genéricas en las arquitecturas Flux es bastante simple: eliminar la duplicación
siempre que sea posible. Aquí hay una ilustración de una tienda genérica:

Esto permite que el estado y las transformaciones que son comunes a las stores que tienen
características específicas compartan el estado que también es común. De lo contrario, cada ronda
de actualización tendrá que realizar la misma actualización en una store diferente. Es mejor
mantener la actualización en un solo lugar para permitir que las stores consulten la store genérica
para calcular su propio estado.
Es importante señalar que las stores específicas no heredan nada de las stores
genéricas de la misma manera que una clase hijo heredaría las propiedades de sus
padres. Piense en las stores genéricas como instancias, como cualquier otra store.
Además, como cualquier otra store, las stores genéricas reciben acciones del
dispatcher para calcular los cambios de estado.

Registro de stores genéricas


Con dependencias de datos, como las que eventualmente encontraremos con las stores en nuestras
arquitecturas Flux, el orden importa. Por ejemplo, si una stores específica se procesa antes que una
stores de la que depende en una ronda de actualización, podríamos acabar con resultados
inesperados. La store genérica siempre necesita procesar primero las acciones para que tenga la
oportunidad de realizar cualquier transformación y establecer su estado antes de que las stores
dependientes accedan a ella.
Veamos un ejemplo. Primero, implementaremos una store genérica que recoge una colección de
objetos documento y los mapea en una colección con el nombre del documento:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { LOAD_DOC } from '../actions/load-doc';
// La store genérica "Docs" guarda un indice
// de nombres de documentos, desde que son usados
// por otras stores.
class Docs extends EventEmitter {
constructor() {
super();
this.state = [];
dispatcher.register((e) => {
switch(e.type) {
case LOAD_DOC:
// Cuando una acción "LOAD_DOC" es enviada,
// tomamos los datos "payload.docs" y
// los transformamos en el estado genérico que
// es necesitado por muchas stores.
for (let doc of e.payload.docs) {
this.state[doc.id] = doc.name;
}
this.emit('change', this.state);
break;
}
});
}
}
export default new Docs();
A continuación, implementaremos una store específica que depende de esta store genérica de Docs.
Será un documento específico, que es utilizado por una página que muestra el nombre del
documento. Esta store tendrá que localizar el nombre basado en la propiedad id, en la store
genérica:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import docs from './docs';
import { LOAD_DOC } from '../actions/load-doc';

// Store específica que depende de la store


// genérica "docs".
class Doc extends EventEmitter {
constructor() {
super();
this.state = {
name: ''
};
dispatcher.register((e) => {
switch(e.type) {
case LOAD_DOC:
// Identificador del documento...
let { id } = e.payload;
// Aquí es dónde los datos de la store genérica
// son prácticos – sólo nos tenemos que preocupar del
// nombre del documento. Podemos usar el "id"
// para consultar en la store genérica..
this.state.name = docs.state[id];
this.emit('change', this.state);
break;
}
});
}
}
export default new Doc();
Paremos un momento y pensemos en lo que hemos hecho aquí y por qué lo estamos haciendo. Esta
store genérica Docs implementa una transformación que mapea una colección de datos de
documentos a una serie de nombres. Hacemos esto porque tenemos varias otras stores que necesitan
buscar un nombre de documento por id . Si sólo fuera la store Doc la que necesita estos datos, esto
difícilmente valdría la pena ponerlo en práctica. La idea es reducir la duplicación, no para introducir
referencias indirectas.
Dicho esto, echemos un vistazo a una función creadora de acciones que escuchará ambas stores:
import dispatcher from '../dispatcher';
// Identificador de acción...
export const LOAD_DOC = 'LOAD_DOC';
// Carga el nombre de un documento concreto.
export function loadDoc(id) {
// La API devuelve un documento sin transformar...
new Promise((resolve, reject) => {
resolve([
{ id: 1, name: 'Doc 1' },
{ id: 2, name: 'Doc 2' },
{ id: 3, name: 'Doc 3' }
]);
}).then((docs) => {
// El payload contiene colecciones de
// documentos sin transformar y “id” de documentos específicos.
// La tienda genérica “docs” usa los datos
// sin transformar "docs" mientras la tienda específica dependa
// de esta colección genérica.
dispatcher.dispatch({
type: LOAD_DOC,
payload: {
id: id,
docs: docs
}
});
});
}
Como puedes ver, esta función toma un identificador de documento como parámetro y realiza una
llamada asíncrona para cargar todos los documentos. Una vez cargados, se envía la acción
LOAD_DOC y las dos stores pueden establecer su estado. El desafío entonces se convierte en:
¿cómo nos aseguramos de que la store genérica se actualice antes que cualquier store que dependa
de ella? Echemos un vistazo al módulo main.js y veamos cómo funciona este creador de
acciones, junto con las dos stores:
// Tenemos que importar el generíco "docsStore", incluso aunque
// no lo estemos usando aquí, así puede registrar con
// el dispatcher y responder a las acciones "LOAD_DOC".
import docsStore from './stores/docs';
import docStore from './stores/doc';
import { loadDoc } from './actions/load-doc';
// Registra los datos que nuestra store específica obtiene de
// la store generíca.
docStore.on('change', (state) => {
console.log('name', `"${state.name}"`);
});
// Carga el documento con identificador 2.
loadDoc(2);
// → name "Doc 2"
Cuando se llama loadDoc(2), la store específica obtiene su estado como esperamos. Esto sólo
funciona debido al orden en el que estamos importando las dos stores a main.js. De hecho, si
cambiáramos la importación e importáramos docStore antes que docsStore, entonces no
veríamos los resultados que esperamos. La razón es simple: el orden en que la las stores se registran
en el dispatcher determina el orden en que se procesan las acciones. Más adelante en el capítulo,
veremos un enfoque menos engorroso para manejar relaciones entre stores.

Combinación de datos genéricos y específicos


Lo bueno de las stores genéricas es que pueden ser utilizadas directamente por las vistas. Es decir,
no son un concepto abstracto. Estas mismas tiendas también pueden ser utilizadas por stores más
específicas para ampliar sus datos y transformar su estado en algo que requieran otras vistas.
Veamos un ejemplo donde una store específica combina el estado de una tienda más general con su
propio estado. Empezaremos buscando en la store de un grupo genérico:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { LOAD_GROUPS } from '../actions/load-groups';
// Store genérica para grupos de usuarios...
class Groups extends EventEmitter {
constructor() {
super();
this.state = [];
dispatcher.register((e) => {
switch(e.type) {
// Almacena el payload de un array de grupos tal cual son.
case LOAD_GROUPS:
this.state = e.payload;
this.emit('change', this.state);
break;
}
});
}
}
export default new Groups();
No hay mucho que hacer aquí con respecto a la transformación del estado - la store sólo establece el
payload como su estado. Ahora, veremos que la store users es más específica, ya que depende de la
store groups:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import groups from './groups';
import { LOAD_USERS } from '../actions/load-users';

// Store users que depende de la store


// genérica groups así puede realizar las transformaciones
// de estado necesarias.
class Users extends EventEmitter {
constructor() {
super();
this.state = [];
dispatcher.register((e) => {
switch(e.type) {
case LOAD_USERS:
// Solo queremos los usuarios disponibles.
let users = e.payload.filter(
x => x.enabled);
// Mapea un nuevo array de usuarios, cada objeto user
// contiene una nueva propiedad “groupName”. Esta
// provendrá de la store genérica group, y es
// consultada por el identificador.
this.state = users.map(
x =>Object.assign({
groupName: groups.state.find(
y =>y.id === x.group
).name
}, x));
this.emit('change', this.state);
break;
}
});
}
}
export default new Users();
La transformación del estado que ocurre en esta store está un poco más involucrada. El payload
LOAD_USERS es un array de objetos user, cada uno con una propiedad group. Sin embargo, las
vistas que observan esta store tienen una necesidad específica del nombre de group, no del id. Por lo
tanto, es aquí donde realizamos el mapeo que crea un nuevo array de objetos user, esta vez con la
propiedad groupName requerida por nuestras vistas. He aquí el detalle de la función creadora de
acciones loadUsers():
import dispatcher from '../dispatcher';
// Indetificador de la acción...
export const LOAD_USERS = 'LOAD_USERS';
// Envía la acción "LOAD_USERS" una vez
// los datos asíncronos han sido devueltos.
export function loadUsers() {
new Promise((resolve, reject) => {
resolve([
{ group: 1, enabled: true, name: 'User 1' },
{ group: 2, enabled: false, name: 'User 2' },
{ group: 2, enabled: true, name: 'User 3' }
]);
}).then((users) => {
dispatcher.dispatch({
type: LOAD_USERS,
payload: users
});
});
}
Y aquí como se cargan los datos genéricos de groups, seguido de los datos de users los cuales
dependen de ellos:
import groupsStore from './stores/groups';
import usersStore from './stores/users';
import { loadGroups } from './actions/load-groups';
import { loadUsers } from './actions/load-users';

// Registra el estado de "usersStore" para asegurar


// que incluye datos de la genérica
// "groupsStore"
usersStore.on('change', (state) => {
state.forEach(({ name, groupName }) => {
console.log(`${name} (${groupName})`);
});
});
// Siempre cargaremos los datos genéricos primero. Especialmente
// si no cambian a menudo.
loadGroups();
loadUsers();
// →
// User 1 (Group 1)
// User 3 (Group 2)
Los datos de stores genéricas como éste son especialmente útiles si son utilizados por muchas otras
stores específicas, y si su estado no cambia con frecuencia. Por ejemplo, cargar estos datos de store
genérica podría ser parte de las actividades de inicialización de la aplicación, y no necesita ser
tocado después.

Gestión de dependencias entre stores


Hasta ahora en este libro, hemos tratado nuestras dependencias de las stores Flux implícitamente.
Con el orden en el que importamos los módulos de store determinamos el orden en el que se
manejaban las acciones, lo que tiene implicaciones si algo de lo que dependemos no ha sido
actualizado todavía. Es hora de empezar a tratar las dependencias de nuestras stores con un poco
más de rigor.
En esta sección, presentaremos el mecanismo waitFor() del dispatcher Flux para gestionar
relaciones entre stores. A continuación, hablaremos de dos tipos de dependencias de stores que
podríamos tener. La primera clase de dependencia está estrictamente relacionada con los datos de la
aplicación. El segundo tipo de dependencia está relacionado con los elementos de la interfaz de
usuario.

A la espera de las stores


El dispatcher tiene un mecanismo incorporado que nos permite resolver explícitamente las
dependencias de la store. Además, las relaciones se declaran directamente en la función de
devolución de llamada, donde se utiliza realmente la dependencia. Veamos un ejemplo que resalta el
código mejorado para tratar las dependencias de store. Primero, tenemos una store básica que no
hace mucho:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { MY_ACTION } from '../actions/my-action';
class Second extends EventEmitter {
constructor() {
super();
// Registra la llamada con el dispatcher
// devolviendo un identificador...
this.id = dispatcher.register((e) => {
switch(e.type) {
case MY_ACTION:
this.emit('change');
break;
}
});
}
}
export default new Second();
Notarás que algo en esta store se ve ligeramente diferente. Estamos asignando el valor de retorno de
dispatcher.register() a la propiedad id de la store. Este valor se utiliza para identificar la
función de devolución de llamada que acabamos de registrar en el dispatcher. Ahora, definamos una
store que dependa de ésta para que podamos ver por qué esta propiedad id es relevante:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { MY_ACTION } from '../actions/my-action';
import second from './second';

class First extends EventEmitter {


constructor() {
super();
// Registro de un callback con el dispatcher
// devolviendo un identificador...
this.id = dispatcher.register((e) => {
switch(e.type) {
case MY_ACTION:
// Esto dice al dispatcher que procese cualquier
// función callback que haya sido registrada
// en "second.id" antes de continuar.
dispatcher.waitFor([ second.id ]);
this.emit('change');
break;
}
});
}
}
export default new First();
La propiedad id es usada por la llamada dispatcher.waitFor(). Este método del dispatcher
obliga a que las acciones sean enviadas a las stores de las que dependen antes de continuar con las
transformaciones de estado. Esto asegura que siempre estemos trabajando con los datos más
actualizados en las stores de las que dependemos. Veamos la función myAction() en uso, y si la
gestión de dependencias entre nuestras dos tiendas está funcionando como se esperaba:
// El orden de importación de store ya no importa,
// desde que las stores usan el dispatcher para
// manejar explícitamente la resolución de dependencias.
import first from './stores/first';
import second from './stores/second';
import { myAction } from './actions/my-action';
// La primera store cambió...
first.on('change', () => {
console.log('first store changed');
});
// La segunda store cambió...
second.on('change', () => {
console.log('second store changed');
});
// Envío de "MY_ACTION"...
myAction();
Ya no importa en qué orden suceden las cosas en main.js , o en cualquier otro lugar de la
arquitectura. La dependencia se declara donde importa, cerca del código que está usando los datos
dependientes. Esto es ejecutado por el componente dispatcher.
Tenga en cuenta que el método waitFor() acepta un array de IDs. Esto significa
que en escenarios más complejos donde dependemos del estado de más de una store,
podemos pasar el ID de cada store de la que dependemos. Sin embargo, el caso más
común es depender del estado de una store. Si hay dependencias de varias stores por
toda la arquitectura, es un signo de demasiada complejidad.

Dependencias de datos
Hay dos tipos de dependencias que vale la pena considerar en las stores Flux. Las más comunes son
las dependencias de datos. Este es el tipo de dependencia que existe cuando un una store específica
depende de una store genérica, tiene algunos datos genéricos a las que varias stores tienen necesidad
de acceso. Estos datos de la aplicación generalmente provienen de una API y son en última
instancia renderizadas por una vista. Sin embargo, no estamos restringidos a stores genéricas
cuando estamos hablando de dependencias de datos.
Por ejemplo, digamos que tenemos una interfaz de usuario y el diseño principal está separado por
pestañas. Las stores de nuestra arquitectura Flux están, como es lógico, alineadas con estas
pestañas. Si una de estas stores realiza una llamada a la API, entonces realiza algunas
transformaciones de para establecer su estado, ¿puede otra store depender de esta para usar estos
datos? Tendría sentido compartir datos de esta manera, de lo contrario, tendríamos que repetir la
misma solicitud a la API, transformar los datos, etc. Esto se vuelve repetitivo y nos gustaría evitarlo.
Sin embargo, cuando las stores que modelan explícitamente funcionalidades de nivel superior como
pestañas, empezamos a notar otras dependencias que no están estrictamente relacionadas con los
datos. Estas son dependencias de la UI, y es perfectamente factible tenerlas. Por ejemplo, lo que el
usuario ve en una pestaña podría depender del estado de una casilla de verificación de otra pestaña.
Aquí hay una ilustración de las dos clases de relaciones de store:

Dependencias de la interfaz de usuario


En arquitecturas típicas de frontend, el estado de los elementos de interfaz de usuario es
probablemente el aspecto más propenso a errores del modelado de estados. El principal problema
con los elementos de la interfaz de usuario es que cuando no modelamos explícitamente sus estados,
nos cuesta entender la causa y el efecto cuando esos estados cambian. Esto se vuelve
particularmente problemático cuando el estado de un elemento de la UI depende del estado de otro.
Terminamos con un código que vincula implícitamente estos elementos.
Las stores Flux son mejores para tratar este tipo de dependencia porque en una store, una interfaz de
usuario es lo mismo que una dependencia de datos: es sólo un estado. Es una cosa buena que
podemos hacer fácilmente en arquitecturas Flux, porque este tipo de dependencias tienden a
volverse complejas rápidamente. Para ilustrar cómo Flux trata con las dependencias de interfaz de
usuario, veamos un ejemplo. Crearemos dos tiendas para diferentes secciones de la interfaz de
usuario: una para las casillas de verificación y otra para las etiquetas. La idea es que las etiquetas
dependan del estado de las casillas de verificación, porque su estilo cambia a medida que cambian
las casillas de verificación.
Primero, tenemos la store que representa los elementos de las casillas de verificación:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { FIRST } from '../actions/first';
import { SECOND } from '../actions/second';

class Checkboxes extends EventEmitter {


constructor() {
super();
this.state = {
first: true,
second: true
};
// Inicializa el id que envía esta store
// para que otras stores puedan depender de ella.
// Dependiendo de la acción, este manejador
// cambia el stado booleando de la UI dado por un
// checkbox.
this.id = dispatcher.register((e) => {
switch(e.type) {
case FIRST:
this.state.first = e.payload;
this.emit('change', this.state);
break;
case SECOND:
this.state.second = e.payload;
this.emit('change', this.state);
break;
}
});
}
}
export default new Checkboxes();
Hay dos elementos de la casilla de verificación modelados por esta store -first y second. El
estado es booleano, verdadero cuando está marcado, falso cuando no lo está. Por defecto, ambas
casillas están marcadas, y cuando se envían las acciones FIRST o SECOND, el estado de la casilla
respectiva se actualiza para reflejar el payload. Ahora veamos la store de Labels, que depende del
estado de la store Checkboxes:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { FIRST } from '../actions/first';
import { SECOND } from '../actions/second';
import checkboxes from './checkboxes';

class Labels extends EventEmitter {


constructor() {
super();
// El estado inicial de esta store depende
// del estado inicial del store
// "checkboxes".
this.state = {
first: checkboxes.state.first ?
'line-through' : 'none',
second: checkboxes.state.second ?
'line-through' : 'none'
};
this.id = dispatcher.register((e) => {
switch(e.type) {
// La acción "FIRST" fue enviada, espera
// por el estado de los "checkboxes" de la UI, luego actualiza
// el estado de la etiqueta "first" en la UI.
case FIRST:
dispatcher.waitFor([ checkboxes.id ]);
this.state.first = checkboxes.state.first ?
'line-through' : 'none';
this.emit('change', this.state);
break;
// La acción "SECOND" fue enviada, espera
// por el estado de los "checkboxes" de la UI, luego actualiza
// el estado de la etiqueta “second” en la UI.
case SECOND:
dispatcher.waitFor([ checkboxes.id ]);
this.state.second = checkboxes.state.second ?
'line-through' : 'none';
this.emit('change', this.state);
break;
}
});
}
}
export default new Labels();
Puedes ver aquí que incluso el estado inicial de esta store depende del estado de la store
Checkboxes. El valor de las propiedades del estado de first o second en esta tienda son en
realidad valores CSS. Es importante que modelemos estos valores aquí, porque esto es el estado,
después de todo el estado entra en una store. Esto significa que más tarde otra cosa puede depender
de estos valores. Cuando todo es explícito, sabemos por qué las cosas son como son, lo que se
traduce en software estable.
Ahora, veamos las vistas que usan estas stores para renderizar los elementos UI y para responder a
las entradas del usuario. Primero, la vista Checkboxes:
import checkboxes from '../stores/checkboxes';
import { first } from '../actions/first';
import { second } from '../actions/second';
class Checkboxes {
constructor() {
// Los elementos DOM que esta vista manipula (son
// checkboxes).
this.first = document.getElementById('first');
this.second = document.getElementById('second');
// Envía la apropiada acción cuando cualquiera
// de los checkboxes cambian. El payload de la acción
// es la propiedad "checked" del elemento de la UI.
this.first.addEventListener('change', (e) => {
first(e.target.checked);
});
this.second.addEventListener('change', (e) => {
second(e.target.checked);
});
// Cuando el store "checkboxes" cambie su estado,
// renderiza la vista usando el nuevo estado.
checkboxes.on('change', (state) => {
this.render(state);
});
}
// Asigna las propieades "checked" de los elementos
// checkbox de la UI. Por defecto, usamos el estado
// inicial de las stores "checkboxes" store. Sin embargo,
// usamos cualquier estado que le sea pasado.
render(state=checkboxes.state) {
this.first.checked = state.first;
this.second.checked = state.second;
}
}
export default new Checkboxes();
Aquí se utilizan dos elementos de casillas de verificación, y lo primero que se hace en el constructor
de la vista es configurar los manejadores de eventos de cambio para las casillas de verificación.
Estos manejadores enviarán la acción apropiada – FIRST o SECOND - dependiendo de la casilla
de verificación y su estado marcado. La función render() actualiza el DOM basado en el
estado. Ahora, veamos la vista Labels:
import labels from '../stores/labels';
class Labels {
constructor() {
// Los elementos DOM que esta vista manipula(son
// labels).
this.first = document.querySelector('[for="first"]');
this.second = document.querySelector('[for="second"]');
// Cuando la store "labels" cambie, renderiza
// la vista usando el nuevo estado.
labels.on('change', (state) => {
this.render(state);
});
}
// Actualiza el estilo "textDecoration" de nuestros
// elementos label de la UI, usando el estado de
// la store "labels" como predeterminado. Sin embargo, usaremos cualquier
// estado que le sea pasado.
render(state=labels.state) {
this.first.style.textDecoration = state.first;
this.second.style.textDecoration = state.second;
}
}
export default new Labels();
Esta vista funciona de forma similar a la vista Checkboxes. Las principales diferencias son las
siguientes, no hay interactividad de usuario aquí, y que los cambios hechos a los elementos de la
interfaz de usuario son los valores de propiedad de estilo que se establecieron en la store Labels.
Éstos dependen en última instancia del estado de la store Checkboxes, de modo que si el usuario
cambia el estado de la casilla de verificación, verán que la etiqueta correspondiente cambia de
estilo.
Si esto se siente como mucho código para lograr algo simple, es porque lo es. Recuerde, en realidad
hemos logrado mucho más aquí que un simple botón de selección y una actualización de estilo de
una etiqueta. Hemos establecido dependencias explícitas del estado de la interfaz de usuario entre
dos secciones diferentes de la interfaz de usuario. Esto es una victoria para nuestra arquitectura,
porque el primer momento en que una determinada arquitectura se esfuerza por escalar es cuando
no podemos averiguar por qué algo sucede. A lo largo de la vida útil de una arquitectura Flux,
tomamos activamente pasos para asegurarnos de que esto no suceda, como acabamos de demostrar
aquí.

Ver orden de actualización


Aunque es bueno poder controlar explícitamente las dependencias de nuestras stores usando
waitFor() , las vistas no tienen tales lujos. En esta sección, veremos el orden en el que nuestras
vistas muestran los elementos de la interfaz de usuario. En primer lugar, veremos el papel que
deben desempeñar las stores en el orden de las actualizaciones de las vistas. A continuación,
repasaremos los casos en los que el orden de visualización afecta realmente a la experiencia del
usuario frente a aquellos en los que no importa.

Orden de registro de la store


El orden en el que se envían las acciones a las stores es importante. Cuando una store transforma su
estado, también notifica a cualquier vista que esté escuchando. Esto significa que si una vista está
escuchando una store que fue registrada primero con el dispatcher, esta vista se mostrará antes que
cualquier otra vista. La idea se ilustra aquí:

Como puedes ver, el orden de las funciones de llamada de retorno de store dentro del dispatcher
influye claramente en el orden de representación de las vistas. Las dependencias de la store también
pueden afectar al orden de representación de las vistas. Por ejemplo, si la store A depende de la
store B, cualquier vista que escuche la store B se renderizará primero. Puede ser que nada de esto
importe o que haya algunos efectos secundarios interesantes. A continuación, analizaremos ambos
resultados.

Priorización en la renderización de vistas


Dado que las stores que forman el núcleo de nuestra arquitectura Flux también pueden determinar el
orden de presentación de nuestras vistas, tenemos que tener cuidado de priorizar según la
experiencia de usuario. Por ejemplo, podríamos tener una store que represente el área de cabecera
superior de la página y otra store que es para el área de contenido principal. Ahora, si el contenido
principal se muestra primero, dejando un hueco notable cerca de la parte superior de la página,
vamos a querer arreglar eso.
Viendo como los usuarios comenzarán en la parte superior de la página y trabajarán hacia abajo,
tendríamos que asegurarnos de que la store para el contenido del encabezado esté registrada
primero. ¿Cómo lo hacemos? Una vez más, volvemos a estar donde estábamos cuando tratábamos
con relaciones entre stores. Debemos importar nuestras vistas en el orden correcto-un orden que
refleja el orden de entrega. Como vimos con las stores, esta no es una situación ideal en la que estar.
Una respuesta es introducir una relación de dependencia entre store. Aunque la store del contenido
en realidad no utiliza ningún dato de la store de cabecera, todavía podría depender de ella para el
propósito de orden de renderizado. Usando el método waitFor(), sabríamos que cual de las
vistas que escuchan la store de encabezados se mostrará primero, eliminando la posibilidad de
problemas de usabilidad relacionados con el orden de renderizado. El riesgo aquí, por supuesto, es
el mismo que cualquier otra dependencia-la complejidad de la store. Cuando llegamos al punto en
que hay demasiadas dependencias entre stores para comprender fácilmente, es hora de repensar el
diseño de nuestra store.

Lidiando con la complejidad de la store


El principal culpable de la complejidad de las stores Flux es la gestión de las dependencias. A pesar
de tener el dispatcher como una herramienta para manejar estas dependencias, algo se pierde
cuando hay demasiadas. En esta sección final del capítulo, discutiremos las consecuencias de tener
demasiadas stores en nuestra arquitectura y lo que se puede hacer para remediar la situación.

Demasiadas stores
Las funcionalidades de nivel superior de nuestra aplicación hacen un trabajo decente de
proporcionar un límite para nuestras stores y el estado que encapsulan. El reto con las stores es
cuando hay demasiadas de ellas. Por ejemplo, a medida que nuestras aplicaciones crecen con el
tiempo, se construirán más funcionalidades, lo que se traducirá en más stores que serán incluídas en
la arquitectura. Además, las stores que ya existen son propensas a volverse más complejas también,
ya que tienen que encontrar maneras de llevarse bien con todas las otras funcionalidades de la
aplicación.
Esto hace que el escenario sea cada vez más complejo: la complejidad en las stores y más stores en
general. Esto casi con toda seguridad conducirá a una explosión de dependencias, ya que
atomizamos todos los casos posibles de nuestra interfaz de usuario. Las stores genéricas que son
compartidas por muchos otras stores específicas también pueden ser una fuente de problemas. Por
ejemplo, podríamos terminar con demasiadas stores genéricas, llegando eventualmente al punto en
que todo nuestro estado los datos son indirectos.
Cuando hayamos llegado al punto en el que el número de stores en nuestra arquitectura es
insostenible, es hora de empezar a repensar lo que constituye una funcionalidad.

Repensar los dominios de las funcionalidades


Tener un mapa de funcionalidades de alto nivel para nuestras stores por lo general funciona lo
suficientemente bien, hasta que se tienen muchas funcionalidades de alto nivel. En este punto, es
hora de reevaluar la política de un mapa de funcionalidades por store. Por ejemplo, si tenemos
muchas funcionalidades de alto nivel, las probabilidades son que hay una gran cantidad de datos
similares que podrían ser recogidos en una sola store que manejo muchas funcionalidades. Otro
efecto potencial de reducir el número de stores que alimentan nuestras funcionalidades es la
eliminación de las stores genéricas. Las stores genéricas sólo son buenas si tenemos demasiados
datos duplicados, lo que tiende a ser menos problemático cuando el número de stores disminuye. A
continuación se muestra un diagrama que muestra cómo una store podría ser el manejador de más
más de una funcionalidad:
También podríamos encontrarnos en la situación opuesta, en la que la complejidad de una store es
simplemente demasiado grande, y necesitamos reducir sus responsabilidades refactorizándola en
múltiples stores. Para arreglar esto, tenemos que pensar en cómo una funcionalidad grande puede
ser convertida en dos funcionalidades más pequeñas. Si no podemos pensar en una buena manera de
dividir la funcionalidad, entonces tal vez la complejidad de la tienda es lo mejor que podemos
hacer, y debería dejarse en paz.

Sumario
En este capítulo, hemos echado un vistazo detallado a las stores en arquitecturas Flux, empezando
con los aspectos que es más probable que cambien una vez que hayamos pasado de la fase
arquitectura esqueleto. Luego introdujimos la noción de stores genéricas, siendo la idea para reducir
la cantidad de estados que las stores individuales tienen que mantener. La parte incómoda de las
stores genéricas son las dependencias que introducen, y para tratar con ellas, aprendiste a usar el
mecanismo waitFor() del dispatcher.
Las dependencias entre stores vienen en dos variedades, dependencias de datos y UI; y aprendió que
las dependencias de UI son una parte crítica de cualquier arquitectura Flux. Por último, discutimos
algunas de las maneras en que las stores pueden crecer fuera de control en términos de complejidad,
y lo que se puede hacer al respecto. En el siguiente capítulo, vamos a ver los componentes de vista
en las arquitecturas Flux.
7
Viendo información
La capa de vista es la última parada de flujo de datos en una arquitectura Flux. Las vistas son la
esencia de nuestra aplicación porque proporcionan información directamente al usuario y responden
a sus necesidades a través de las interacciones del propio usuario. Este capítulo examina
detalladamente los componentes de la vista en el contexto de una arquitectura Flux.
Comenzaremos con una discusión sobre cómo obtener vistas de sus datos y lo que pueden hacer,
una vez que los obtengan. A continuación, veremos algunos ejemplos que hacen hincapié en la
naturaleza apátrida de las vistas de Flux. Luego, revisaremos las responsabilidades de las vistas en
Flux que son diferentes de las vistas en otros tipos de arquitecturas frontend.
Terminaremos el capítulo con un vistazo al uso de los componentes de ReactJS como capa de vista.
Vamos a empezar!

Pasar los datos de las vistas


Las vistas no tienen su propia fuente de datos que puedan utilizar para renderizar elementos de
interfaz de usuario. En su lugar, confían en el estado de las stores Flux y escuchan los cambios de
estado. En esta sección, cubriremos el evento de cambio que emitirán las stores para indicar que las
vistas pueden representarse a sí mismas. También discutiremos la idea de que en última instancia
depende de la vista decidir cuándo y cómo renderizar la interfaz de usuario.

Datos mediante un evento de modificación


Todos los componentes de vista que hemos visto hasta ahora en este libro se han basado en el
evento de cambio que emiten las stores cuando el estado de una store ha cambiado. Así es como la
vista sabe que puede renderizarse en el DOM, porque hay un nuevo estado de la store, lo que
significa que probablemente haya un cambio visual que queremos que el usuario vea.
Puede que hayas notado en los ejemplos anteriores que todas las funciones del manejador que
escuchan eventos de cambio tienen un parámetro de estado - éste es el estado de la store. La
pregunta es: ¿por qué necesitamos incluir estos datos de estado? ¿Por qué la vista no puede
simplemente hacer referencia a la store directamente para hacer referencia a los datos de estado?
Esta idea se ilustra aquí:

El evento de modificación sigue siendo necesario, aunque la vista haga referencia directa al estado
de la store. Se emite el evento de cambio y la vista sabe entonces que el estado al que se refiere
también ha cambiado. Hay un problema potencial con este enfoque, y tiene que ver con la
inmutabilidad. Veamos algún código para entender mejor el problema. Aquí hay una store la
propiedad name como su estado:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { NAME_CAPS } from '../actions/name-caps';
class First extends EventEmitter {
constructor() {
super();
// El estado de la propiedad "name"
// es un string en minúsculas.
this.state = {
name: 'foo'
};
this.id = dispatcher.register((e) => {
switch(e.type) {
// Cambia la propiedad "name", manteniendo
// el objeto "state" intacto.
case NAME_CAPS:
let { state } = this;
state.name = state.name.toUpperCase();
this.emit('change', state);
break;
}
});
}
}
export default new First();
Cuando esta store responde a la acción NAME_CAPS, su trabajo es transformar el estado de la
propiedad name, usando una simple llamada a toUpperCase() . A continuación, el evento de
modificación es emitida con el estado como dato del evento. Veamos otra store que hace lo mismo
pero usando un enfoque diferente para actualizar el objeto state:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { NAME_CAPS } from '../actions/name-caps';
class Second extends EventEmitter {
constructor() {
super();
// El estado por defecto es una propiedad name
// que es un string en minúsculas.
this.state = {
name: 'foo'
};
this.id = dispatcher.register((e) => {
switch(e.type) {
// Asigna un nuevo objeto "state", invalidando
// cualquier referencia a algún estado anterior.
case NAME_CAPS:
this.state = {
name: this.state.name.toUpperCase()
};
this.emit('change', this.state);
break;
}
});
}
}
export default new Second();
Como puedes ver, las dos stores son básicamente idénticas y producen el mismo resultado cuando
se envía la acción NAME_CAPS. Sin embargo, ten en cuenta que esta transformación no muta el
objeto state. Lo reemplaza en su lugar. Este enfoque mantiene el objeto de estado inmutable, lo
que significa que la store nunca cambiará ninguna de sus propiedades. La diferencia se siente en la
capa de vista, y resalta la necesidad del argumento estado en el manejador de eventos de cambio:
import first from './stores/first';
import second from './stores/second';
import { nameCaps } from './actions/name-caps';
// Prepara las referencias al estado de las
// dos stores.
var firstState = first.state;
var secondState = second.state;
first.on('change', () => {
console.log('firstState', firstState.name);
});
// → firstState FOO
second.on('change', () => {
console.log('secondState', secondState.name);
});
// → secondState foo
second.on('change', (state) => {
console.log('state', state.name);
});
// → state FOO
nameCaps();
Por eso no podemos hacer suposiciones sobre el estado de una store. En el código anterior,
acabamos de cometer un error crítico al asumir que podíamos mantener una referencia a
secondStore.state. Resulta que este objeto es inmutable, por lo que la única forma de que las
vistas accedan al nuevo estado es a través del argumento estado en el manejador de modificaciones.
Las vistas deciden cuándo renderizar
El trabajo de un store Flux se centra principalmente en generar la información correcta para
consumo de las vistas. Lo que no es parte de la descripción del trabajo de una store es saber cuando
una vista realmente necesita actualizarse o no. Esto significa que depende de la vista decidir qué
ocurre cuando una store desencadena un evento de cambio - podría ser que nada en el DOM
necesite ser actualizado. La pregunta entonces es: ¿por qué una store emitiría un cambio si nada ha
cambiado?
La respuesta simple es que las stores no contabiliza si algo ha cambiado o no. La store sabe cómo
realizar las transformaciones de estado correctas, pero no necesariamente hace un seguimiento de
los estados anteriores y sus diferentes propósitos, aunque ciertamente podría hacerlo.
Veamos una store que no mute su estado. En cambio, crea un nuevo estado cuando algo se
transforma:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { NAME_CAPS } from '../actions/name-caps';
class MyStore extends EventEmitter {
constructor() {
super();
this.state = {
name: 'foo'
};
this.id = dispatcher.register((e) => {
switch(e.type) {
case NAME_CAPS:
// Convierte a mayúsculas.
let name = this.state.name.toUpperCase();
// Solo asigna el nuevo objetos estado si
// "name" no está ya en mayúsculas.
this.state = this.state.name === name ?
this.state : {
name: this.state.name.toUpperCase()
};
// Informa a las vistas sobre el cambio de estado, incluso
// si el objeto estado es el mismo.
this.emit('change', this.state);
break;
}
});
}
}
export default new MyStore();
Esta store está escuchando el mismo mensaje NAME_CAPS del ejemplo anterior. Su labor sigue
siendo la misma-transformar la propiedad name a mayúsculas. Sin embargo, este código funciona
de forma diferente que en la última versión de la store. Es inmutable en el sentido de que no cambia
el objeto estado, lo reemplaza. Pero sólo lo hace si el valor a sido realmente cambiado. De lo
contrario, el objeto state permanece igual. La idea aquí no es mostrar que las stores deberían
detectar los cambios de estado en las propiedades individuales, sino que los pueden emitir incluso
cuando el estado no ha cambiado. En otras palabras, nuestras vistas no deberían hacer la suposición
de que la entrega al DOM es necesaria, sólo por un evento de cambio.
Ahora volvamos nuestra atención a la vista. El plan es simple, no lo hagas a menos que haya que
hacerlo:
import myStore from '../stores/my-store';
class MyView {
constructor() {
// La vista guarda una copia del estado
// previo del store.
this.previousState = null;
myStore.on('change', (state) => {
// Asegura que tenemos un nuevo estado antes
// de renderizar. Si “state” es igual a
// “previousState”, entonces sabemos qee no hay
// nada que renderizar.
If (state !== this.previousState) {
console.log('name', state.name);
}
// Mantiene una referencia del estado, así
// podremos usarla en el próximo evento
// “change”.
this.previousState = state;
});
}
}
export default new MyView();
Puede ver que la propiedad previousState mantenga una referencia al estado de la propiedad
de store. Pero espera, ¿no es eso algo malo, según la sección anterior a ésta? Bueno, no, porque en
realidad no estamos usando la referencia para nada más que un estricto control de la equidad. Esto
se usa para determinar si la vista necesita ser renderizada o no. Dado que el estado de store es
inmutable, podemos afirmar que si se pasa la misma referencia como argumento para el gestor de
eventos de cambio, nada ha cambiado realmente y podemos ignorar el evento de forma segura.
Veamos qué sucede cuando llamamos a la misma acción varias veces en sucesión:
import myView from './views/my-view';
import { nameCaps } from './actions/name-caps';
// A pesar de las repetidas llamadas a "nameCaps()",
// "myView" solo se renderiza una vez.
nameCaps();
nameCaps();
nameCaps();
// → name FOO
Más adelante en este capítulo, cuando veamos ReactJS, veremos escenarios más avanzados de
vistas que sólo representan lo que necesitan. Más adelante en el libro, cuando veamos
Immutable.js, abordaremos la detección de cambios de estado más avanzados.

Mantener las vistas sin estado


Las vistas no pueden estar completamente sin estado porque interactúan con el DOM, y los
elementos DOM asociados a una vista siempre tendrán un estado. Sin embargo, podemos tomar
medidas para tratar las vistas como entidades sin estado dentro del contexto de nuestra arquitectura
Flux. En esta sección, trataremos dos aspectos de las vistas sin estado.
Primero, repasaremos la idea de que todo estado en una arquitectura Flux pertenece a una store,
incluyendo cualquier estado de UI que pudiéramos tener la tentación de mantener en los
componentes de nuestra vista. En segundo lugar, veremos la consulta DOM y por qué queremos
evitar hacer esto desde dentro de nuestras vistas Flux.

El estado de la UI pertenece a las stores


Como aprendiste en el capítulo anterior, el estado de la interfaz de usuario es igual al estado que se
deriva de los datos de la aplicación: todo pertenece a una store. El estado de la UI incluye cosas
como la propiedad deshabilitada de un botón o el nombre de una clase que se aplica a un div. La
razón por la que estos pedacitos de estado pertenecen a una store es que otras stores podrían
depender de ellos. Esto a su vez afecta el resultado de la representación de otras vistas. Esta clase de
dependencia se visualiza como se indica a continuación:

Si el estado de la UI de las que otras stores podrían depender no proviene de una store, entonces
tiene que depender de la vista o el propio DOM. Esto es inconsistente y va en contra de lo que Flux
significa: ordenar actualizaciones estrictas y mantener el estado confinado.

Sin necesidad de consultar el DOM


Cuando el estado de la interfaz de usuario se mantiene en una store Flux, no hay necesidad de
consultar el DOM para averiguar si un botón está desactivado o no. Piense en el enfoque de jQuery
para manipular el estado de la aplicación. Primero, tenemos que emitir una consulta DOM que nos
consiga los elementos relevantes del DOM, y luego tenemos que averiguar si están en el estado
apropiado leyendo algunas de sus propiedades. Entonces, podemos hacer cambios en otra parte de
la solicitud. O tal vez hay una mezcla de estados que se mantiene directamente en el DOM y
algunos objetos JavaScript.
Es la consistencia la que marca la mayor diferencia en las arquitecturas Flux, porque no tenemos
que consultar al DOM para obtener la propiedad de un enlace. Las stores que se aferran al estado de
UI ya tienen esta información. Este siempre es el caso, no es una cuestión de averiguar si está en el
DOM o en algún otro componente.
Otra ventaja de tener todo el estado de UI que necesitamos para hacer renderizado en nuestras stores
es que no hay cuellos de botella en el rendimiento. Consultar del DOM una o dos veces no es gran
cosa, y esto tiene que pasar si vamos a mostrar cambios para el usuario. Lo que no queremos es
tener una larga serie de llamadas de consulta DOM que ni siquiera dan como resultado que se
renderice algo. En otras palabras, no hay necesidad de consultar el DOM para extraer información
cuando ya debería estar en una store.
Esta es la misma estrategia que utilizan las tecnologías de árbol DOM virtual como ReactJS, donde
todos los datos de DOM se almacenan en objetos JavaScript. Buscar algún estado de la interfaz de
usuario desde un objeto JavaScript es intrínsecamente más rápido que buscar propiedades de
elementos DOM, y así es como ReactJS es capaz de funcionar tan bien minimizando el número de
interacciones DOM para un determinado cambio en la interfaz de usuario.

Ver responsabilidades
En este punto del libro, probablemente tengas un buen manejo del papel de ver componentes en una
arquitectura Flux. En pocas palabras, su trabajo es mostrar la información de la store a los usuarios
insertándola en el DOM. En esta sección, vamos a romper este concepto de visión central en tres
partes.
Primero está la entrada de las vistas: los datos de la store. A continuación, tenemos la estructura de
la vista en sí misma, y las diversas formas en que puede descomponerse en vistas más pequeñas.
Por último, está la interactividad con el usuario. Cada una de estas tres áreas de componentes de la
vista tiene una relación con el flujo de datos a través de nuestra arquitectura Flux. Veamos cada una
de las ahora.

Renderización de datos de store


Si la store transforma los datos en información que el usuario necesita, entonces ¿por qué tener
vistas? ¿Por qué no hacer que las stores transmitan directamente la información al DOM?
Necesitamos vistas por un par de razones. En primer lugar, una store podría ser usada en varios
lugares, representado por varias vistas. Segundo, Flux no es necesariamente el que se ocupa de la
presentación visual de la información. Por ejemplo, si tuviéramos que diseñar alguna vista que no
renderiza HTML sino algún otro formato de visualización.
Las vistas no mantienen ningún estado ni realizan ninguna transformación en la información de la
store. Sin embargo, necesitan transformar un poco la información, para convertirla en válida, para
su visualización en el navegador o cualquier otro medio de visualización en el que nuestra
aplicación se ejecuta. Pero aparte de marcar la información devuelta por las stores, las vistas tienen
poco que hacer. Es la tecnología de visualización, como ReactJS, la que hace que la mayoría las
consultas para recolectar información en términos de crear objetos JavaScript e insertarlos en el
DOM. Aquí hay un diagrama que muestra el proceso:
Estructura de vista parcial
El objetivo de las stores en las arquitecturas de Flux es estructurarlas de forma que sólo haya una
por cada funcionalidad de nivel superior. Esto nos permite sortear los problemas creados por el
hecho de tener una enorme estructura de jerarquías de datos. Las vistas, por otro lado, pueden
beneficiarse de un poco de una estructura jerárquica. Sólo porque una funcionalidad de alto nivel se
basa en información de una sola store, no significa que sólo una sola vista pueda dirigir la
experiencia del usuario.
Al principio del libro, discutimos la noción de estructura jerárquica y cómo ésta debe evitarse en
arquitecturas Flux. Esto sigue siendo cierto hasta cierto punto en lo que respecta a las vistas, porque
no importa cómo se divida, las jerarquías profundas son difíciles de comprender. Las vistas
necesitan ser descompuestas hasta cierto punto, porque de lo contrario terminaremos poniendo toda
la complejidad en un solo lugar. El HTML es jerárquico por naturaleza, así que hasta cierto punto
nuestras vistas deberían imitar esta estructura, como se ilustra aquí:

Al igual que las stores pueden ser genéricas, también lo pueden ser las vistas. Más de una
funcionalidad puede ser genérica para presentar información utilizando un patrón de visualización
común. Por ejemplo, piensa en algún tipo de panel expandible/colapsable que sea usado por todos
nuestras funcionalidad-¿no tendría sentido conectar esto a nuestras funcionalidades más grandes en
lugar de duplicar la funcionalidad? La tecnología de visualización que estamos usando también es
decisiva en cómo queremos descomponer nuestras vistas en piezas reutilizables más pequeñas, ya
que esto es más fácil de hacer con algunos frameworks que con otros. Por ejemplo, veremos en la
siguiente sección que ReactJS hace que sea fácil componer vistas más globales a partir de vistas
más pequeñas y refinadas porque son en gran parte autónomas.
Algo que hay que tener en cuenta al componer jerarquías de vistas como ésta: el
flujo de datos. Por ejemplo, cuando un store de Flux cambia, emite el evento de
cambio para que la vista de nivel superior pueda representarse a sí misma. Luego
se extiende a sus hijos inmediatos, que lo extiende a sus hijos inmediatos, y así
sucesivamente. A medida que el estado de la store fluye a través de estas vistas, no
deberían producirse transformaciones de datos en el camino. Dicho de otra
manera, las vistas finales en el árbol DOM deberían obtener la misma información
que la vista raíz.
Interactividad con el usuario
El área final de responsabilidad de la vista en la que tenemos que pensar es la interactividad del
usuario. Aparte de la observación pasiva de la información que cambia en sus pantallas por cambios
en las stores subyacentes de nuestra arquitectura a través de acciones, ellos necesitarán hacer cosas.
Los usuarios necesitan ser capaces de navegar alrededor de la aplicación para utilizar las diversas
funciones que hemos implementado. Para manejar este tipo de cosas, la vista que renderizan la
interfaz de usuario también deberían interceptar los eventos DOM ya que es donde se
desencadenan. Esto generalmente resulta en el envío de una nueva acción, como ya hemos visto
antes en este libro.
La clave a recordar sobre estos manejadores de eventos es que deben tener esencialmente una
responsabilidad: llamar a la función creadora de la acción correcta. Lo que los manejadores de
eventos deben evitar es tratar de ejecutar cualquier lógica - esto pertenece a una store, junto con el
estado al que afecta la lógica. Esto es tan fundamental para Flux que es es muy posible que lo repita
al menos doce veces más en el libro. Una vez que empecemos introduciendo la lógica en lugares
que no sean stores, perdemos la capacidad de razonar sobre el estado de algo, y el estado determina
en gran medida lo que el usuario ve.
Es totalmente plausible pasar las funciones de creación de acciones directamente como
manejadores de eventos a los nodos DOM. Esto podría ayudarnos, porque ofrece muy
pocas posibilidades de que la lógica se introduzca en el lugar equivocado.

Uso de ReactJS con Flux


ReactJS es una biblioteca para crear componentes de vista. De hecho, React ni siquiera se etiqueta
como una biblioteca de vistas: es un conjunto de herramientas para crear componentes que
renderizan los elementos de la interfaz de usuario. Esta premisa simple es fácil de entender y
poderosa - un ajuste perfecto como tecnología de visualización en nuestra arquitectura Flux.
En esta sección, veremos cómo hacer de ReactJS la tecnología de elección para las vistas en
nuestras aplicaciones Flux, comenzando con la transmisión de información de estado de las stores a
los componentes React. A continuación, hablaremos de la composición de las vistas y de cómo el
estado de Flux fluye de stores a vistas superiores y a vistas inferiores. Por último, implementaremos
algún evento en nuestras vistas usando los mecanismos React y un enrutador usando la directiva
biblioteca react­router.

Ajustado del estado de la vista


Hay dos formas de renderizar los componentes de React en función del estado de nuestras stores
Flux. Estos involucran dos tipos diferentes de componentes -con estado y sin estaodo- de los cuales
hablaremos aquí. En primer lugar, echemos un vistazo a la store que contiene el estado que impulsa
nuestras vistas:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { ADD } from '../actions/add';
class MyStore extends EventEmitter {
constructor() {
super();
// El estado "items" es un array vacío
// por defecto...
this.state = {
items: []
};
this.id = dispatcher.register((e) => {
switch(e.type) {
// Incluye el "payload" en el array
// "items" cuando la acción "ADD" es
// enviada.
case ADD:
let { state } = this;
state.items.push(e.payload);
this.emit('change', state);
break;
}
});
}
}
export default new MyStore();
La idea aquí es simple: cada vez que se envía una acción ADD, añadimos el payload de acción al
array. Cualquier componente React que desee responder a este cambio de estado de la store puede
hacerlo escuchando el evento de cambio. Primero, veamos un componente React que muestra la
lista de elementos:
import { default as React, Component } from 'react';
import myStore from '../stores/my-store';
// Un componente React con estado que se basa en
// su estado para renderizar modificaciones.
export default class Stateful extends Component {
constructor() {
super();
// Cuando "myStore" cambia, asignamos el estado de
// este componente llamando "setState()", causando
// el renderizado.
myStore.on('change', (state) => {
this.setState(state);
});
// El estado inicial del componente es
// recogido desde el estado inicial del store Flux.
this.state = myStore.state;
}
// Renderiza una lista de elementos.
render() {
return (
<ul>
{this.state.items.map(item =>
<li key={item}>{item}</li>)}
</ul>
);
}
}
Este es un componente típico de React, creado usando la sintaxis de ES2015 y extendiendo de la
clase base Component de React. Este enfoque es necesario para los componentes con estado.
Como puedes ver, el constructor de este componente interactúa directamente con una store Flux.
Cuando la store cambia, llama a setState() , que es el comando para reflejar el nuevo estado de
la store. El constructor también fija el valor inicial estableciendo la propiedad del estado. A
continuación, tenemos el método render(), que devuelve los elementos React basados en este
estado.
Tenga en cuenta que nuestro componente React utiliza sintaxis JSX para definir elementos. No
mostramos cómo funciona esto en este libro, ni cubriremos otros aspectos de React en ningún nivel
de detalle. Este es un libro sobre arquitectura Flux, y cubriremos partes de React que son relevantes
en un contexto de Flux. Si quieres una inmersión más profunda sobre React en sí, hay muchos
recursos gratuitos, así como muchos otros libros sobre el asunto.
Ahora veamos otra implementación del mismo componente, lo que significa exactamente la misma
salida. Este es el enfoque sin estado de los componentes/vista de React:
import React from 'react';
// La versión sin estado del componente
// React es una versión más stripped-down
// reducida a lo esencial de un componenente. Solo
// confía en las propiedades que le han
// pasado, puede ser una básica función arrow
// que devuelve un elemento React.
export default ({ items }) => (
<ul>
{items.map(item =>
<li key={item}>{item}</li>)}
</ul>
);
Espera, ¿qué? Este es exactamente el mismo componente, sólo que no depende del estado. Esto
podría ser algo bueno si lo estamos implementando como un componente de vista dentro de nuestra
arquitectura Flux. Lo que más destaca de esta implementación es que hay más comentarios que
código, lo que es bueno, lo que nos permite centrarnos en la estructura DOM resultante. Notará que
no hay interacción con una store Flux en este módulo. Recuerde, este es un componente de React
sin estado, una simple función arrow, lo que significa que no tenemos ningún método de ciclo de
vida que definir, incluyendo el estado inicial. Esto está bien; veamos cómo usamos ambos tipos de
componentes en nuestro módulo main.js:
import React from 'react';
import { render } from 'react-dom';
import Stateful from './views/stateful';
import Stateless from './views/stateless';
import myStore from './stores/my-store';
import { add } from './actions/add';

// Estos son los “containers” de elementos DOM que


// nuestros componentes React van a renderizar.
var statefulContainer =
document.getElementById('stateful');
var statelessContainer =
document.getElementById('stateless');
// Establece el listener de cambios de nuestro
// componente React "Stateless". Es una simple
// llamada "render()", React maneja eficientemente
// la semántica diferida del DOM.
myStore.on('change', (state) => {
render(
<Stateless items={myStore.state.items}/>,
statelessContainer
);
});
// Renderizado inicial de nuestros dos componentes.
render(
<Stateful/>,
statefulContainer
);
render(
<Stateless items={myStore.state.items}/>,
statelessContainer
);
// Envía algunas acciones, causando que nuestra store cambie,
// y nuestros componentes React se re-rendericen.
add('first');
add('second');
add('third');
La diferencia clave aquí es que la vista Stateless necesita tener sus interacciones con la store
configuradas manualmente aquí. El componente sin estado encapsula esto configurando el listener
de cambio en el constructor.
¿Es un enfoque superior al otro? Dentro de una arquitectura Flux, los componentes sin estado de
React tienden a tener una ventaja sobre su contraparte con estado. Esto se debe al simple hecho de
que hacen cumplir la idea de que el estado pertenece a las stores, a ninguna otra parte. Cuando
nuestros componentes de React son funciones simples, no tenemos más remedio que averiguar la
forma correcta de transformar el estado de la store en algo que pueda consumirse como simples
propiedades inmutables.

Componer vistas
Al igual que el estado de nuestra aplicación se compone en stores, las vistas de ese estado se
componen jerárquicamente hasta cierto punto. Digo hasta cierto punto porque queremos evitar
descomponer la estructura de nuestra interfaz de usuario a un nivel profundo, ya que esto sólo
dificulta su comprensión. Donde la composición de la vista realmente importa es cuando tenemos
partes más pequeñas que son utilizadas por muchos componentes más grandes. React es bueno para
componer vistas que no introducen demasiada complejidad. En particular, las vistas sin estado son
una buena manera de mantener el flujo de datos unidireccional a medida que atraviesa los niveles de
la vista. Vamos a ver un ejemplo. Aquí hay una store con algún estado inicial, que ordena este
estado según un acción específica:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { SORT_DESC } from '../actions/sort-desc';
class MyStore extends EventEmitter {
constructor() {
super();
// El estado de la store es un array de
// strings.
this.state = {
items: [
'First',
'Second',
'Third'
]
};
this.id = dispatcher.register((e) => {
switch(e.type) {
// La acción "SORT_DESC" ordena el
// array de "items" en orden descendente.
case SORT_DESC:
let { state } = this;
state.items.sort().reverse();
this.emit('change', state);
break;
}
});
}
}
export default new MyStore();
En este caso, esperaríamos que la matriz sea Third, Second, First (alfabéticamente) cuando se envíe
la acción SORT_DESC. Ahora, veamos el componente de vista principal que escucha esta store:
import React from 'react';
import Item from './item';
// La vista de la aplicación. Renderiza un listado
// componentes "Item".
export default ({ items }) => (
<ul>
{items.map(item =>
<Item key={item}>{item}</Item>)}
</ul>
);
Una vez más, tenemos una vista funcional simple que no se aferra a ningún estado, porque no hay
necesidad - todo el estado se mantiene en las stores Flux. En lugar de usar un elemento li aquí,
estamos usando un componente React personalizado de Item que hemos implementado para
nuestra aplicación. Esto es parte de la vista de una aplicación más grande, y quizás es parte de otras
vistas más grandes. El resultado es la reutilización de código y vistas agregadas simplificadas.
Veamos el componente Item a continuación:
import React from 'react';
// Un componente "li" con texto "strong".
export default (props) => (
<li>
<strong>{props.children}</strong>
</li>
);
No es la vista más emocionante del mundo, y en la práctica encontrarás vistas atómicas más
complejas que ésta. Pero la idea es la misma: el valor de las props.children, en última
instancia, proviene de una store Flux, y atraviesa desde la vista padre hasta aquí.. Veamos cómo
encajan todas las piezas en main.js:
import React from 'react';
import { render } from 'react-dom';
import myStore from './stores/my-store';
import App from './views/app';
import { sortDesc } from './actions/sort-desc';
// El elemento contenedor de nuestra aplicación.
var appContainer = document.getElementById('app');
// Renderiza el componente de la vista "App" cuando
// el estado de la store cambia.
myStore.on('change', (state) => {
render(
<App {...state}/>,
appContainer
);
});
// Renderizado inicial...
render(
<App {...myStore.state}/>,
appContainer
);
// Realiza la ordenación descendente...
sortDesc();

Reaccionando a los eventos


Los componentes de React tienen su propio sistema de eventos en ellos. En realidad son una
envoltura alrededor del sistema de eventos DOM, haciendo más fácil para nosotros incluir
funciones de manejo de eventos como parte del lenguaje de marcas de un componente JSX. Esto
también tiene implicaciones para nuestra arquitectura Flux, porque estos eventos a menudo se
traducen directamente en llamadas a funciones creadoras de acciones.
Para tener una idea de los eventos de React en un contexto de Flux, construyamos sobre el ejemplo
anterior. Añadiremos un botón que cambia el orden de los elementos. Pero primero, echaremos un
vistazo a las modificaciones de la store necesarias para soportar este nuevo comportamiento:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { SORT } from '../actions/sort';
// Constantes para la etiqueta de dirección
// del botón ordenar.
const ASC = 'sort ascending';
const DESC = 'sort descending';
class MyStore extends EventEmitter {
constructor() {
super();
// Tenemos algunos "items", y una "direction"
// como estados predeterminados en esta store.
this.state = {
direction: ASC,
items: [
'Second',
'First',
'Third'
]
};
this.id = dispatcher.register((e) => {
switch(e.type) {
case SORT:
let { state } = this;
// Los "items" están siempre ordenados.
state.items.sort()
// Si la actual “direction” es ascendente,
// entonces la modificamos a “DESC”. De otra manera,
// actualizamos a “ASC” y el orden se invierte
if (state.direction === ASC) {
state.direction = DESC;
} else {
state.direction = ASC;
state.items.reverse();
}
this.emit('change', state);
break;
}
});
}
}
export default new MyStore();
Hay un nuevo elemento de estado en MyStore - direction. Es relevante tanto para la dirección
de ordenación de los elementos como para el contenido de texto del botón de ordenación de la vista.
Echemos un vistazo a la nueva vista de la aplicación ahora:
import React from 'react';
import Sort from './sort';
import Item from './item';
// Vista de la aplicación. Renderiza un botón
// de ordenación y una lista de componentes "Item".
export default ({ items, direction }) => (
<div>
<Sort direction={direction}/>
<ul>
{items.map(item =>
<Item key={item}>{item}</Item>)}
</ul>
</div>
);
Puedes ver que el elemento devuelto por esta función sin estado es un div. Aunque no es
estrictamente necesario desde una perspectiva de lenguaje de marcas, es necesario desde una
perspectiva de componente React, las funciones de renderizado sólo pueden devolver un elemento.
El elemento Sort que hemos añadido sobre la lista representa el botón de ordenación. Echemos un
vistazo a este componente ahora:
import React from 'react';
import { sort } from '../actions/sort';
// Algunos estilos en línea para las vistas React...
var style = {
textTransform: 'capitalize'
};
// Renderiza un elemento "button", con el
// estado de store "direction" como etiqueta
// y la función creadora de acciones "sort()"
// será llamada cuando el botón sea pulsado.
export default ({ direction }) => (
<button
style={style}
onClick={sort}>{direction}
</button>
);
Este es un simple elemento button de HTML, con un estilo que pondrá en mayúsculas la etiqueta
direction. También puede ver que la propiedad onClick se utiliza para especificar el
manejador de eventos. En este caso, es simple: llamamos a la función creadora de acciones
sort() directamente cuando se hace clic en el botón.
En la práctica, otras acciones de gestión de estados podrían enviarse de forma
concertada con la acción SORT. Por ejemplo, una acción PRE_SORT puede ser
necesaria para manejar el estado de los botones.

Enrutamiento y acciones
La librería react­router es la solución de enrutamiento de facto de los proyectos ReactJS. Si
estamos usando el componente React para la capa de vista de nuestra arquitectura Flux, entonces
hay una buena posibilidad de que queramos usar este paquete para enrutar en nuestra aplicación. Sin
embargo, hay algunos matices sutiles que hay que tener en cuenta cuando se utiliza react­
router en el contexto de Flux. En esta sección final del capítulo, nos ocuparemos de algunas de
las compensaciones que tenemos que hacer con react­router implementándolo en una
arquitectura Flux.
La premisa básica de react­router es lo que lo hace tan atractivo en primer lugar. El enrutador
y las rutas dentro de él son en sí mismos componentes de React que podemos usar en el DOM.
Podemos declarar que una determinada ruta debe dar lugar a un determinado renderizado cuando se
activa la ruta. El enrutador maneja todos los detalles esenciales para nosotros. La pregunta es,
¿cómo funciona esto en el contexto de una aplicación Flux? Como sabemos, las stores son donde
vive el estado en nuestra aplicación. Así que esto significa que podrían querer saber también sobre
el estado del enrutador.
Comencemos mirando el módulo main.js, donde se encuentra el componente de enrutamiento
declarado y renderizado:
import React from 'react';
import { render } from 'react-dom';
import { Router, Route, IndexRoute } from 'react-router';
import App from './views/app';
import First from './views/first';
import Second from './views/second';
import { routeUpdate } from './actions/route-update';

// El contenedor de elementos de nuestra aplicación.


var appContainer = document.getElementById('app');
// Llamada a"Router" cuando una ruta cambia.
// Aquí es donde llamamos al creador de acciones
// "routeUpdate()", pasandole la
// nueva ruta.
function onUpdate() {
routeUpdate(this.state.location.pathname);
}
// Renderiza los componenentes enrutadores. Cada ruta
// tiene asociada un componente React que es
// renderizado cuando la ruta es activada.
render((
<Router onUpdate={onUpdate}>
<Route path="/" component={App}>
<IndexRoute component={First}/>
<Route path="first" components={First}/>
<Route path="second" component={Second}/>
</Route>
</Router>
), appContainer);
Puedes ver aquí que hay tres rutas principales, la ruta predeterminada /, seguida de una ruta
/first y una /second. Cada ruta tiene un componente correspondiente que se renderiza cuando
la ruta se activa. Lo interesante de estas declaraciones de ruta es que los componentes First y
Second son hijos de App. Esto significa que cuando sus rutas son activadas, son realmente
renderizadas dentro de App . Echemos un vistazo al componente App ahora:
import React from 'react';
import { Link } from 'react-router';
// Renderiza algunos enlaces a las rutas de la aplicación.
// Los "props.children" son algunos sub-componentes.
export default (props) => (
<div>
<ul>
<li><Link to="/first">First</Link></li>
<li><Link to="/second">Second</Link></li>
</ul>
{props.children}
</div>
);
Este componente muestra una lista de enlaces que apuntan a nuestras dos rutas, first y second.
También hace componentes hijos a través de props.children. Aquí es donde se renderizan los
componenentes hijos. Pasemos ahora a la función creadora de acciones routeUpdate(). Esta es
llamada por el componente Router cada vez que la ruta cambia:
import dispatcher from '../dispatcher';
// Identificadores de acciones...
export const ROUTE_UPDATE = 'ROUTE_UPDATE';
export const PRE_ROUTE_UPDATE = 'PRE_ROUTE_UPDATE';
export function routeUpdate(payload) {
// Envía inmediatamente la acción
// "PRE_ROUTE_UPDATE", dando a las stores una oportunidad de ajustar
// sus estados mientras ocurran actividades asíncronas.
dispatcher.dispatch({
type: PRE_ROUTE_UPDATE,
payload: payload
});
// Envía la acción "ROUTE_UPDATE"
// después de un segundo.
setTimeout(() => {
dispatcher.dispatch({
type: ROUTE_UPDATE,
payload: payload
});
}, 1000);
}
En realidad hay dos acciones que son enviadas por esta función. En primer lugar, está la acción
PRE_ROUTE_UPDATE, que se planifica para que las stores tengan la oportunidad de prepararse
para el cambio de ruta. Luego, realizamos un comportamiento asíncrono usando setTimeout()
y luego envía la acción ROUTE_UPDATE. Ahora echemos un vistazo a una de las stores utilizadas
por nuestros componentes. A continuación, veremos una de las vistas que escucha esta store. Las
stores y las vistas son casi idénticas, por lo que no hay necesidad de mirar más de una:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
// Necesitamos un par de constantes de acción desde
// el mismo módulo de acción.
import {
PRE_ROUTE_UPDATE,
ROUTE_UPDATE
} from '../actions/route-update';
class First extends EventEmitter {
constructor() {
super();
// El estado "content" es inicialmente un
// string vacío.
this.state = {
content: ''
};
this.id = dispatcher.register((e) => {
let { state } = this;
switch(e.type) {
// La acción "PRE_ROUTE_UPDATE" destina la
// ruta que ha cambiando una vez el
// código asíncrono en el creador de acciones ha
// resuelto. Podemos modificar el estado
// “content” aquí.
case PRE_ROUTE_UPDATE:
if (e.payload.endsWith('first')) {
state.content = 'Loading...';
this.emit('change', state);
}
break;
// Cuando la acción "ROUTE_UPDATE" es enviada,
// podemos cambiar el contenido a mostrar que ha sido
// cargado.
case ROUTE_UPDATE:
if (e.payload.endsWith('first')) {
state.content = 'First Loaded';
this.emit('change', state);
}
break;
}
});
}
}
export default new First();
La store actualiza su estado de contenido según el tipo de acción y la ruta actual. Esto es importante
porque otras áreas de la aplicación pueden querer saber que esta store está esperando que se
complete una actualización de ruta. Ahora veamos la vista que escucha esta store:
import { default as React, Component } from 'react';
import first from '../stores/first';
export default class First extends Component {
constructor() {
super();
// El estado inicial viene desde una store.
this.state = first.state;
// La función callback “change” de la store está
// definida aquí así que puede ser asignada a
// "this" y asignar el componente estado.
this.onChange = (state) => {
this.setState(state);
};
}
// Renderiza el HTML usando “content".
render() {
return (
<p>{this.state.content}</p>
);
}
// Establece el listener “change” de la store.
componentWillMount() {
first.on('change', this.onChange);
}
// Elimina el listener “change” de la store.
componentWillUnmount() {
first.removeListener('change', this.onChange);
}
}
Este componente tiene estado porque tiene que tenerlo. Ya que es el enrutador el que renderiza el
componente inicialmente, no podemos volver a renderizarlo si no es configurando su estado. Así es
como podemos volver a renderizar el componente para reflejar el estado de la store, configurando
un controlador para el evento de modificación. Este componente también tiene métodos de ciclo de
vida para escuchar el evento de cambio de la store, así como para eliminar el listener. Si nosotros no
lo eliminamos, intentaría establecer el estado de un componente que no está montado.

Sumario
Este capítulo entró en detalle en la capa de vista de las arquitecturas de Flux. Comenzando con la
obtención de información en la vistas, aprendiste que el evento de cambio es fundamental al
reflejar el estado de la store en la vista, y que las vistas suelen leer directamente las stores durante
su renderización inicial. Luego, repasamos la idea de que las vistas son sin estado. El estado de un
elemento de interfaz de usuario determinado pertenece a una store, ya que otras partes del archivo
puede depender de este estado, y no queremos tener que consultar el DOM.
A continuación, repasamos algunas de las responsabilidades de alto nivel de los componentes de la
vista. Esto incluye renderizar la información de la store, componer estructuras de vista más grandes
con componentes de vista más pequeños, y el manejo de la interactividad del usuario. Finalizamos
el capítulo con un resumen del uso de los componentes de ReactJS como vista en una arquitectura
Flux. En el siguiente capítulo, profundizaremos en el ciclo de vida de los componentes de Flux y en
qué se diferencian de otras arquitecturas.
8
Ciclo de vida de la información
Todo sistema de información tiene un ciclo de vida. Componentes individuales en estos sistemas
también tienen sus propios ciclos de vida. Acumulativamente, estos pueden ser fáciles de tratar o
abrumadoramente difícil. En las arquitecturas frontend JavaScript, la tendencia es hacia lo último.
La razón es simple, los ciclos de vida que tienen nuestros componentes, fundamentalmente alteran
el flujo de información a través del tiempo de maneras que son casi imposible de predecir.
Este capítulo trata sobre el ciclo de vida de la información en arquitecturas Flux. Flux es diferente
de otras arquitecturas en que pone énfasis en el escalado de la información en lugar de en los
componentes JavaScript. Comenzaremos explorando este tema con una mirada a las dificultades
que nos hemos enfrentado durante años, utilizando los ciclos de vida típicos de los componentes
que se encuentran en modernos frameworks JavaScript. Luego, contrastaremos este enfoque con el
de Flux, donde los componentes de alto nivel son relativamente estáticos.
A continuación, nos adentraremos en el concepto de escalar la información y cómo esto conduce a
más arquitecturas sanas que son mucho más fáciles de mantener que los enfoques alternativos.
Cerraremos el capítulo con una discusión sobre stores inactivas-aquellas que no están ofreciendo
una vista con datos. Vayamos al grano.

Dificultades en el ciclo de vida de los componentes


Un aspecto de la ampliación de una arquitectura de interfaz es la limpieza de los recursos no
utilizados. Esto libera memoria para los nuevos recursos que se crean a medida que el usuario
interactúa con la aplicación. JavaScript es un recolector de basura, lo que significa que una vez que
un objeto no tiene alguna referencia a él, es susceptible para ser recolectado la próxima vez que el
recolector se ejecute. Esto nos lleva parcialmente a que, no hay necesidad de asignar/desasignar
manualmente memoria. Sin embargo, tenemos otra categoría de problemas de escalado, y todos
ellos son relacionados con el ciclo de vida de los componentes.
En esta sección, hablaremos de los escenarios en los que queremos recuperar los recursos no
utilizados y cómo esto ocurre generalmente en las arquitecturas frontend. A continuación,
analizaremos los desafíos que presentan las dependencias de componentes en el contexto de la
gestión del ciclo de vida. Finalmente, veremos los escenarios de fuga de memoria. Incluso con las
mejores herramientas, siempre existe la posibilidad de que nuestro código haya hecho algo para
eludir la gestión de la memoria.

Recuperación de recursos no utilizados


Algo que pasa mucho a lo largo del curso de una aplicación, es que se crean nuevos recursos
mientras que los recursos antiguos se destruyen. Esto es en respuesta a la interactividad del usuario
- a medida que atraviesan las funcionalidades de la aplicación, los nuevos componentes son creados
para presentar nueva información. Gran parte de esta creación y destrucción de objetos JavaScript y
elementos DOM es transparente para nosotros - las herramientas que empleamos pueden encárgase
de esto por nosotros. El siguiente diagrama captura la idea de un componente que libera recursos
internos a medida que cambia de estado:

La clave está en el ciclo de vida de nuestros componentes. Dependiendo del marco que es
responsable de la gestión de este ciclo de vida, pueden ocurrir diferentes cosas en diferentes lugares
y momentos. Por ejemplo, tu componente es instanciado y almacenado cuando su componente
padre es creado. Cuando tu componente es renderizado, inserta un nuevo DOM y guarda una
referencia a ellos. Finalmente, cuando el padre del componente es destruido, nuestro componente se
instruye para quitar sus elementos del DOM y para lanzar cualquier referencias a ellos. Este es un
flujo de trabajo demasiado simplificado, pero la idea general es que no importa cuántas piezas
móviles haya. El trabajo de las herramientas que utilizamos es gestionar el ciclo de vida de nuestros
componentes de forma que se recuperen los componentes no utilizados.
¿Por qué es tan importante recuperar los componentes no utilizados? La limitación fundamental a la
que nos enfrentamos es que la memoria es finita, y estamos tratando de construir una aplicación
robusta que pueda escalar bien. La eliminación de componentes de la memoria cuando ya no se
necesitan, deja espacio para que se creen nuevos componentes cuando se necesitan. Entonces, ¿cuál
es el gran problema si estamos usando un framework que tiene ciclos de vida bien definidos para
nuestros componentes y maneja muchos de los detalles sucios para nosotros?
Un factor limitante de este enfoque es que con una aplicación compleja que tiene muchas partes
móviles, el framework está constantemente creando y destruyendo objetos. Esto inevitablemente
lleva a que el recolector de basura sea invocado con frecuencia, causando pausas en el hilo de
ejecución principal de JavaScript. En el peor de los casos, esto puede llevar a pausas en la
experiencia de usuario debido a la falta de respuesta de los eventos de usuario. El otro escollo
potencial de la los ciclos de vida de los componentes gestionados automáticamente es que el
framework no siempre sabe lo que estamos pensando, y esto puede llevar a dependencias ocultas
que terminan rompiendo el flujo del componente en el ciclo de vida de crear/destruir.

Dependencias ocultas
Los patrones que definen el ciclo de vida de un tipo particular de componente son una buena
opción, siempre y cuando nuestros componentes cumplan con su ciclo de vida el cien por cien de
las veces. Esto rara vez funciona porque estamos tratando de construir algo único que resuelve un
problema para nuestros usuarios, no una pieza de software que se reproduce con un framework por
el simple hecho de serlo. El mayor riesgo aquí es que evitar accidentalmente que el framework
libere recursos de forma adecuada mediante la introducción de dependencias. Estas dependencias
podrían tener perfecto sentido en el contexto de nuestra aplicación pero en lo que al framework se
refiere, no sabe nada de ellos, y esto se rompe de maneras impredecibles. Echa un vistazo al
siguiente diagrama:

Los escenarios reales a los que nos enfrentamos serán un poco más matizados que el escenario
representado aquí. El tema general es que los frameworks que manejan los ciclos de vida son
implacables. Todo lo que se necesita es una dependencia en el lugar equivocado para invalidar
completamente todo lo que el framework está haciendo para la aplicación. Sin embargo, este es el
riesgo/beneficio de tener ciclos de vida para los componentes arquitectónicos. El beneficio es que
necesitamos reclamar componentes para dar paso a otros nuevos, y si un framework puede
automatizar esta ardua tarea para nosotros, mucho mejor. El riesgo es que cada vez que las cosas se
crean y destruyen, existe la posibilidad de que esto no se haga correctamente, lo que lleva a fugas de
memoria.

Fugas de memoria
Cuando nuestro código está constantemente creando y destruyendo objetos, el recolector de basura
de JavaScript se trilla, y experimentamos cortes en el funcionamiento. Sin embargo, se trata de un
problema menor en comparación con las fugas de los componentes JavaScript que nunca son
totalmente recogidos por el recolector de basura. Esto tiende a suceder cuando nuestro código de
aplicación tiene ideas que no encajan del todo con las del framework que gestiona el ciclo de vida
de nuestros componentes. Obviamente las fugas de memoria son un gran problema de escalabilidad
que se debe evitar a toda costa.
Así que lo que tenemos son dos problemas de escalabilidad relacionados con el ciclo de vida en
nuestra arquitectura. Primero, no queremos crear y destruir constantemente porque esto tiene
problemas de pausas en la recolección de basura. Segundo, no queremos fugas de memoria
introduciendo dependencias ocultas que el framework no es consciente de ello, rompiendo el ciclo
de vida previsto. Como veremos en la siguiente sección, Las arquitecturas de Flux ayudan con
ambos aspectos de los problemas del ciclo de vida de los componentes. No hay mucha
creación/destrucción de componentes en una arquitectura Flux. Esto reduce la probabilidad de
introducir una lógica que rompa el ciclo de vida de un componente dado. Más tarde en el capítulo,
veremos cómo Flux se centra en la información en lugar de en los componentes JavaScript para
lograr el escalado.

Las estructuras de Flux son estáticas


Dado que la necesidad de crear y destruir objetos constantemente presenta una oportunidad para
escalar los problemas, parece que deberíamos crear y destruir lo menos posible. Resulta que las
arquitecturas de Flux son diferentes en esta área ya que gran parte de sus infraestructura de
componentes es estática.
En esta sección, veremos lo que distingue a Flux de otras arquitecturas comenzando con el patrón
singleton que es usado por muchos módulos. Entonces, compararemos el enfoque tradicional del
modelo MVC con las stores Flux. Por último, vamos a echar un vistazo a los componentes de vista
estáticos y ver si esta es una idea que vale la pena perseguir para alcanzar la escalabilidad.

Patrón Singleton
Como ya habrán notado, la mayoría de los módulos con los que hemos trabajado hasta ahora en este
libro han exportado una sola instancia. El dispatcher expone una sola instancia de la clase
Dispatcher del paquete Flux de Facebook. Este es el patrón singleton en acción.
La idea básica es que sólo hay una instancia de una clase, crear más nunca es necesario porque la
primera instancia es todo lo que siempre necesitaremos. Esto es un buen presagio con los problemas
de escalabilidad que hemos discutido en este capítulo, donde la creación constante y la destrucción
hace que nuestro código sea vulnerable a errores. Estos errores, en última instancia, evitan que haya
fugas de memoria o problemas de rendimiento.
En cambio, las arquitecturas de Flux tienden a ensamblar la fontanería entre los componentes en el
momento de la puesta en marcha, y esta fontanería se mantiene en su lugar permanentemente.
Piensa en la fontanería física donde vives, está inactiva cuando no está siendo usada. Sin embargo,
el costo de arrancar la fontanería física para reclamar el espacio, y el costo de reemplazarlo cuando
es necesario simplemente no tiene sentido. La sobrecarga de tener estáticas las estructuras de
fontanería dentro de nuestras paredes no son un cuello de botella en la escalabilidad de nuestro día a
día en nuestras vidas.
Así que mientras que podemos evitar algo de la creación y destrucción de objetos siguiendo el
patrón de singleton, hay compensaciones. Por ejemplo, el patrón único no es necesariamente un
buen patrón. Al menos no en todos nuestros módulos donde todo es una clase, y sin embargo, todo
es instanciado una sola vez. Veamos un módulo de la store y veremos si podemos implementar algo
que no requiera una store. Primero, implementemos un módulo de store típico que exporte una
instancia de clase singleton para comparación:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { MY_ACTION } from '../actions/my-action';
// Una típica clase store de Flux. Este módulo
// exporta una instancia singleton de ella.
class SingletonStore extends EventEmitter {
constructor() {
super();
this.state = {
pending: true
};
this.id = dispatcher.register((e) => {
switch(e.type) {
case MY_ACTION:
this.state.pending = false;
this.emit('change', this.state);
break;
}
});
}
}
export default new SingletonStore();
Sólo hay un puñado de propiedades que el mundo exterior requiere de este módulo. Necesita el
estado, para que otros componentes puedan leerlo. Necesita el identificador de registro del
dispatcher, por lo que otros componentes pueden depender de él usando waitFor() . Y, necesita
el EventEmitter , para que otros componentes puedan escuchar los cambios de estado de la
store. Ahora implementemos una store que no requiera instanciar una nueva clase:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { MY_ACTION } from '../actions/my-action';
// Exporta el estado de esta store...
export var state = {
pending: true
};
// Exporta el "id" de registro del dispatcher
// así otras stores pueden depender de este módulo.
export var id = dispatcher.register((e) => {
switch(e.type) {
case MY_ACTION:
state.pending = false;
emitter.emit('change', state);
break;
}
});
// Necesitamos crear un nuevo "EventEmitter" aquí
// ya que no hay clase para extenderla.
const emitter = new EventEmitter();
// Esporta la interfaz mínima que las vistas
// necesitan para escuchar/no escuchar las stores.
export const on = emitter.on.bind(emitter);
export const off = emitter.removeListener.bind(emitter);
Como puedes ver, estamos exportando las necesidades básicas que permiten a otros componentes
tratar este módulo como una store. Y de hecho es una store, está estructurada simplemente de
manera diferente. En lugar de exportar una instancia de clase singleton, que tiene la interfaz
esencial de la store, estamos exportando directamente las piezas de la interfaz. ¿Hay alguna ventaja
fundamental en cualquiera de los dos enfoques? No, no la hay. Si prefieres clases y la habilidad de
extender de una clase base, siga el patrón singleton. Si sientes que son feas e innecesarias, siga el
enfoque modular.
Al fin y al cabo, el resultado arquitectónico es el mismo. La store simplemente existe. No hay
necesidad de crear y destruir la store, ya que el usuario interactúa con la aplicación. No hay nada
que nos impida hacer esto-creando y eliminado tantas stores como cambios de estado tenga la
aplicación. Pero como veremos más adelante realmente no hay ninguna ventaja en hacer esto, así
como no hay ninguna ventaja en destrozar tus paredes cuando el fregadero no funciona.
Veamos estas dos stores en acción. Aparte de cómo se importan, son indistinguibles:
import { myAction } from './actions/my-action';
import singletonStore from './stores/singleton-store';
// Advierta que "moduleStore" es un módulo, con todas
// sus exportaciones, no es una instancia de clase.
import * as moduleStore from './stores/module-store';
// Registra una callback "change" con la store
// singleton...
singletonStore.on('change', (state) => {
console.log('singleton', state.pending);
});
// Registra una callback "change" con el módulo
// store. No se ve ni se siente como
// una instancia de clase exactamente.
moduleStore.on('change', (state) => {
console.log('module', state.pending);
});
// Dispara la acción "MY_ACTION".
myAction();

Comparación con los modelos


¿Recuerda la idea de que las stores representan funcionalidades en el nivel superior de nuestra
aplicación? Bueno, las funcionalidades de alto nivel generalmente no se crean ni se destruyen
constantemente en todas partes de la vida útil de la aplicación. Los modelos por otro lado, nuestros
amigos de la familia de las arquitecturas MV*, a menudo representan dominios de datos más
refinados. Debido a esto, necesitan crearse y destruirse.
Por ejemplo, supongamos que estamos en la página de búsqueda de una aplicación, y que hay un
puñado de resultados visualizados. Los resultados individuales son probables modelos, que
representan la devolución de una API. La vista que muestra los resultados de la búsqueda
probablemente sabe cómo mostrar estos módulos. Cuando los resultados cambian o el usuario
navega a otra parte de la aplicación, los modelos son inevitablemente destruidos. Esto ya fue
previamente discutido en este capítulo. No es una simple eliminación: hay pasos de limpieza que
deben realizarse.
Con las stores Flux, no tenemos el mismo nivel de complejidad. Hay vistas que escuchan una store
determinada, pero eso es todo. Cuando el estado de una store cambia, como cuando los datos del
resultado de la búsqueda se borran del estado de la store, se notifica a las vistas. Es entonces que la
vista tiene que reflejar estos datos modificados volviendo a mostrar la interfaz de usuario. Con Flux,
la limpieza es un simple problema de borrado, tanto desde el punto de vista del DOM como de la
store. El hecho de que no estemos inflando stores enteras mientras el usuario interactúa con la
aplicación significa que hay menos posibilidades de que nuestros componentes arquitectónicos
pierdan la sincronización entre ellos.
Vistas estáticas
Dado que las vistas son los componentes responsables de renderizar la información para el usuario,
tiene sentido que la vista se limpie cuando el usuario no está mirandola, ¿verdad? Bueno, no
necesariamente. Revisando la analogía de la fontanería, cuando nos vamos de la cocina, cerramos el
grifo del fregadero. No conseguimos una caja de herramientas y empezamos a sacar las tuberías. La
noción de que las vistas en una arquitectura Flux pueden ser estáticas es de hecho viable. Es el agua
que tenemos que cerrar para escalar, no las cañerías.
Veamos algunas vistas que se crean al inicio y que nunca se destruyen cuando el usuario interactúa
con la aplicación. Primero, implementaremos una vista estática basada en clases:
import myStore from '../stores/my-store';
class ClassView {
constructor() {
// El elemento DOM "container" para esta vista.
this.container =
document.getElementById('class-view');
// Renderiza el nuevo estado cuando "myStore" cambia.
myStore.on('change', (state) => {
this.render(state);
});
}
render({ classContent } = myStore.state) {
// Establece el contenido del elemento contenedor.
// Esto se hace reduciendo la array
// "classContent" a una simple string. Si está vacía,
// ningún elemento DOM existente será borrado del
// contenedor.
this.container.innerHTML = classContent.reduce(
(x, y) => `<strong>${x + y}</strong>`, '');
}
}
export default new ClassView();
Esto se parece a la clase típica que se encuentra en una arquitectura Flux. Es instanciado dentro del
módulo y exportado. El contenido en sí mismo se renderiza reduciendo un array a una etiqueta
<strong>. Veremos por qué estamos renderizando una etiqueta como esta cuando veamos la
store. Pero primero, veamos otra vista estática que toma la forma de una función:
import React from 'react';
// Renderiza el contenido de la vista usando un componente
// funcional de React.
export default ({content}) => (
<strong>{content}</strong>
);
Este es el estilo funcional de los componentes de React que se te presentaron en el capítulo anterior.
Como puedes ver, no es gran cosa, ya que React se encarga de muchos de los trabajos pesados por
nosotros. Ahora echemos un vistazo a la store en la que ambas vistas se basan para obtener
información:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { SHOW_CLASS, SHOW_FUNCTION } from '../actions/show';
class MyStore extends EventEmitter {
constructor() {
super();
// Los contenidos de las propiedades del estado de esta store
// son arrays vacíos, mostrando un contenido vacío.
this.state = {
classContent: [],
functionContent: []
};
this.id = dispatcher.register((e) => {
let {state} = this;
switch(e.type) {
// Si la acción "SHOW_CLASS" ha sido enviada,
// el estado "classContent" obtiene un array con un único
// objeto y el estado "functionContent" obtiene
// un array vacío.
case SHOW_CLASS:
Object.assign(state, {
classContent: [ 'Class View' ],
functionContent: []
});
this.emit('change', state);
break;
// Si la acción "SHOW_FUNCTION" ha sido enviada,
// el estado "functionContent" obtiene un array con un único
// objeto y el estado "classContent" obtiene un
// array vacío.
case SHOW_FUNCTION:
Object.assign(state, {
classContent: [],
functionContent: [ 'Function View' ]
});
this.emit('change', state);
break;
}
});
}
}
export default new MyStore();
Puedes ver que ambas acciones -SHOW_CLASS y SHOW_FUNCTION- se procesan de la misma
manera. Una acción establece un estado mientras borra otra. Vamos a discutir este enfoque aquí por
un momento. Los estados classContent y functionContent utilizan arrays de un solo
elemento para un valor string. Nuestras dos vistas iteran sobre estas arrays-usando map() y
reduce() . La razón por la que lo hacemos de esta manera es para mantener la lógica fuera de las
vistas. La lógica de negocio que opera en las stores debería permanecer en la store. Sin embargo, las
vistas necesitan saber qué mostrar y qué eliminar. Siempre iterando sobre una colección, como un
array, la generación de contenido es consistente y libre de lógica. Veamos cómo se usan ambas
vistas en main.js:
import React from 'react';
import { render } from 'react-dom';
import { showClass, showFunction } from './actions/show';
import myStore from './stores/my-store';
import classView from './views/class';
import FunctionView from './views/function';

// El elemento DOM usado por nuestro componente


// "FunctionView".
var functionContainer = document
.getElementById('function-view');
// Utilidad para renderizar el componente React
// "FunctionView". Llamado por el manejador "change"
// de la store y realizando el renderizado inicial.
function renderFunction(state) {
render(
<FunctionView
content={state.functionContent}/>,
functionContainer
);
}
// Establece el manejador "change" para "FunctionView"...
myStore.on('change', renderFunction);
// Realiza el renderizado inicial de ambas vistas...
classView.render();
renderFunction(myStore.state);
// Envía la accion "SHOW_CLASS".
showClass();
// Espera por un segundo, entonces envía la acción
// "SHOW_FUNCTION".
setTimeout(() => {
showFunction();
}, 1000);
El uso de classView es sencillo. Es importado y renderizado. El manejador del estado de la store
está encapsulado dentro del módulo de visualización. El componente React FunctionView por
otro lado, necesita ser configurado con una función manejadora que sea llamada cuando el estado de
myStore cambie. Técnicamente, esta no es una vista estática, porque es una función que se llama
cada vez que se llama React.render(). Sin embargo, en Flux, se comporta de forma muy
parecida a una vista estática, porque es el renderizado de React el que maneja la creación y
destrucción de los componentes de la vista - nuestro código no está creando o destruyendo nada,
solo componentes pasados a render() .

Escalado de información
Como has visto hasta ahora en este capítulo, Flux no intenta escalar las cosas que no necesitan ser
escaladas. Por ejemplo, las stores y las vistas a menudo se crean sólo una vez durante la puesta en
marcha. Intentar limpiar estos componentes repetidamente a medida que la aplicación cambia de
estado con el tiempo es simplemente propenso a errores. Escalar la información que fluye a través
de nuestros componentes Flux volcará nuestro sistema si no tenemos cuidado.
Comenzaremos esta sección con un vistazo a cómo nuestras arquitecturas Flux pueden escalar bien
por su cuenta, sin cantidades masivas de datos entrando en el sistema. Esto también sirve para
ilustrar la idea de que, de hecho, se trata de dos problemas distintos: la escalada de nuestros
componentes Flux frente a escalar el volumen de datos que nuestra arquitectura es capaz de
procesar. Luego, discutiremos el tema del diseño de nuestras interfaces de usuario, para hacer el
proceso de diseño de componentes escalables. Exploraremos el papel de las acciones de Flux
cuando llegue el momento de escalar nuestro sistema en el siguiente nivel.

¿Qué escala bien?


A medida que nuestra aplicación crece, necesita escalar en respuesta a cosas como nuevas
funcionalidades y conjuntos de datos crecientes. La pregunta es, ¿cuál de estos problemas de
escalado merece más atención? Debería ser el tema con mayor potencial de derribo de nuestro
sistema. En términos generales, esto tiene más que ver con los datos de entrada que con la
configuración de nuestros componentes Flux. Por ejemplo, hay un potencial escalado si estamos
procesando datos de entrada en tiempo polinómico en lugar de tiempo logarítmico.
Esta es la razón por la que nuestra arquitectura Flux no necesita preocuparse por los ciclos de vida y
mantener la fontanería entre los componentes de la misma manera que otras arquitecturas hacen.
¿Tendremos muchos componentes que ocupen más memoria de la necesaria? ¿es esto costoso en
términos de rendimiento? Claro, esto siempre es una consideración, no queremos tener más
componentes de los que necesitamos. En la práctica, este tipo de costes es apenas perceptible por
los usuarios. Veamos el impacto que la infraestructura de un gran componente tiene en el
rendimiento. Primero, la vista:
// Realmente una vista sencilla...
export default class MyView {
constructor(store) {
// No hace nada excepto verificar que hay
// una propiedad estado "result".
store.on('change', ({ result }) => {
console.assert(
Number.isInteger(result),
'MyView'
);
});
}
}
No hay mucho en esta vista porque no tiene por qué haberlo. No estamos probando el rendimiento
de renderizado de la vista en sí, estamos probando la escalabilidad de la arquitectura. Así que todo
lo que se necesita es que la vista exista y pueda escuchar a una store. Estamos pasando la instancia
de la store a través del constructor porque estamos creando varias instancias de esta vista que
escuchan a diferentes stores, como veremos aquí en un momento. Veamos el código de la store a
continuación:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { MY_ACTION } from '../actions/my-action';
// Hemos exportado la clase store en lugar de
// una instancia de este módulo porque el
// módulo principal creará un puñado de ellas.
export default class MyStore extends EventEmitter {
constructor() {
super();
// El EventEmitter piensa que estamos malgastando memoria
// a través de múltiples listeners. Esto evita
// la limitación.
this.setMaxListeners(5000);
this.state = {};
this.id = dispatcher.register((e) => {
let {state} = this;
// Realiza algunas operaciones aritméticas básicas antes de emitir
// el evento "change" con la propiedad estado
// "result".
switch(e.type) {
case MY_ACTION:
state.result = 100000 * e.payload;
this.emit('change', state);
break;
}
});
}
}
Esta es una store bastante básica que hace un cálculo bastante básico cuando se envía MY_ACTION.
Una vez más, esto es intencional. Ahora veamos cómo estos componentes pueden escalar en una
arquitectura Flux sin muchos datos:
import MyStore from './stores/my-store';
import MyView from './views/my-view';
import { myAction } from './actions/my-action';
// Se adhiere a nuestras referentes stores y vistas...
var stores = [];
var views = [];
// Cuantos objetos crear y actions
// enviar...
var storeCount = 100;
var viewCount = 1000;
var actionCount = 10;
// Configura nuestra infraestructura Flux. Esto establece
// todos los listeners de store relevantes y listeners
// de vista. Todos permanecen activos a lo largo del
// ciclo de vida de la aplicación.
console.time('startup');
for (let i = 0; i < storeCount; i++) {
let store = new MyStore();
stores.push(store);
for (let i = 0; i < viewCount; i++) {
views.push(new MyView(store));
}
}
console.timeEnd('startup')
// → startup: 26.286ms
console.log('stores', stores.length);
console.log('views', views.length);
console.log('actions', actionCount);
// →
// stores 100
// views 100000
// actions 10

// Envía la acción. Aquí es donde nosotros tampoco


// tenemos éxito o fracasamos en el escalado de la arquitectura.
console.time('dispatch');
for (let i = 0; i < actionCount; i++) {
myAction();
}
console.timeEnd('dispatch');
// → dispatch: 443.929ms
Estamos midiendo el costo de inicio de la creación de estos componentes y la configuración de sus
listeners, ya que esto normalmente se sumará al costo de inicio de una aplicación Flux. Pero como
podemos ver aquí, preparar todos estos componentes es intrascendente en términos de experiencia
de usuario. La gran prueba viene cuando se envían las acciones.
Esta configuración hace que se produzcan un millón de llamadas de visualización y tarda
aproximadamente medio segundo. Esta es la fontanería de nuestra aplicación, y realmente no nos
beneficia derribarla y volver a ponerlo todo en su sitio más tarde. Este aspecto de la arquitectura
escala bien. Son los datos que entran en el sistema, y la lógica que opera en ella, ese es el verdadero
desafío de escalado. Si tenemos que hacer esta misma prueba de nuevo con un payload de una
acción de un array de 1000 elementos que fue ordenada por la store, podríamos tener un problema.
Abordaremos escenarios de pruebas de rendimiento más detallados en el Capítulo 13,
Pruebas y Benchmarking.

Información mínima requerida


Como acabas de ver, la noción de que los componentes de Flux y sus conexiones pueden ser
definidos estáticamente es válida. Por lo menos, en términos de retos de escalamiento, tener la
fontanería en su lugar no va a ser la cosa que derribe nuestro sistema cuando intentamos escalarlo.
Son los datos que fluyen hacia el sistema, y los medios por los cuales los transformamos en
información para el usuario. Esta es la cosa que es muy difícil de escalar y, por lo tanto, es mejor
que lo hagamos lo menos posible.
Puede sonar trivialmente obvio al principio, pero tener menos información para mostrar escala
mejor. Esto puede pasarse por alto fácilmente porque estamos decididos a crear funciones, no a
medir el volumen de información resultante en nuestras vistas. A veces, esta es el la manera más
efectiva, o posiblemente la única manera, de solucionar problemas de escalamiento.
Cuando estamos diseñando una arquitectura Flux para nuestra aplicación, tenemos que mantener la
escalabilidad de la información en mente. A menudo, el mejor ángulo para ver el problema es desde
la propia interfaz de usuario. Si hay una forma de cortar ciertas cosas, en un esfuerzo por reducir el
desorden, también reducimos la cantidad de información que las vistas necesitan generar.
Potencialmente, podemos eliminar un flujo de datos completo de nuestra aplicación simplemente
cambiando lo que el usuario ve. Las interfaces de usuario poco saturadas se escalan bien.
Otra cosa a la que hay que prestar atención es a la información que se filtra fuera de los
componentes store. Con esto, me refiero a la información que una store genera sin un propósito real.
Esto podría haber sido algo que solía ser relevante para la forma en que funcionaba la vista, pero
cuando la función cambia, nos olvidamos de sacar la información relevante. O, podría simplemente
ser un descuido en el diseño - estamos generando información que la vista no necesita, y ha sido así
desde el primer día. Estos problemas son difíciles de detectar pero fáciles de arreglar. El único
método infalible es auditar periódicamente nuestras vistas para asegurarse de que están
consumiendo la información que necesitan y nada más.

Acciones que escalan


Las acciones son los guardianes de cualquier dato que quiera entrar en nuestro sistema Flux - si no
es un payload de acción, entonces no son datos que nos importen. Las funciones creadoras de
acciones no son problemáticas en el escalado, ya que no hacen mucho. El aspecto más complejo de
ellas es gestionar el comportamiento asíncrono, si es necesario. Pero esto no es un problema
fundamental de escalado, todas las aplicaciones JavaScript tienen un sistema de componentes
asíncronos. Hay dos maneras fundamentales en las que las acciones pueden frustrar nuestros
esfuerzos de ampliación.
La primera es tener demasiadas acciones. Esto significa que hay más oportunidades para error del
programador debido a todas las posibilidades. Se vuelve menos obvio qué creadora de acciones
debe ser usado en según qué contexto. El mismo problema puede ocurrir cuando son pocas acciones
y demasiados parámetros de creación de acciones. Esto inhibe directamente nuestra capacidad de
obtener los datos correctos en las stores de nuestra aplicación.
La segunda forma en que las acciones pueden tropezar cuando intentamos escalar nuestro sistema es
que las funciones creadoras de acciones están haciendo demasiado. Por ejemplo, una creadora de
acción podría intentar filtrar algunos de los datos de respuesta de la API en un esfuerzo por reducir
los datos que se entregan a las stores a través del dispatcher. Esto es enormemente problemático,
porque viola la regla de Flux de que todo estado y toda lógica que cambia el estado pertenece a las
stores.
Es comprensible cómo algo que puede suceder, cuando bajo presión para escalar una aplicación, el
lugar más obvio para solucionar problemas con los datos es en el origen. En en este caso, la fuente
es el manejador de la respuesta AJAX. La mejor manera de manejar esto es ajustar la propia API y
hacer que la función creadora de acciones suministre el archivo para obtener un conjunto de datos
más pequeño. Cuando las transformaciones de estado se mueven hacia afuera de las stores en el
frontend, reducimos la probabilidad de escalar con éxito porque aumentar la probabilidad de que se
produzcan otros problemas.

Stores inactivas
En la sección anterior, exploramos la idea de que podemos tener una relación relativamente estática
en nuestra arquitectura Flux. Esto no es algo que cause preocupaciones sobre la escalabilidad. Más
bien, son las grandes cantidades de datos que se mantienen en nuestras stores. En esta sección final,
cubriremos algunos escenarios en los que tenemos una store con muchos datos como estado, y no
queremos que nuestra aplicación desborde la memoria.
El primer enfoque implica borrar los datos de la store, liberando recursos. Podemos llevar este
enfoque un paso más allá añadiendo heurística a nuestra lógica de store que determina que nada ha
cambiado y que no hay necesidad de tocar el DOM emitiendo un evento de modificación. Por
último, hablaremos de algunos de los efectos secundarios que causan borrar los datos de la store y
cómo tratarlos.

Borrar datos de store


Algo en lo que tenemos que pensar largo y tendido con nuestros componentes Flux es en cómo los
datos que ingresan al sistema eventualmente saldrán del sistema. Si sólo ponemos los datos sin
sacar nada de ellos, tenemos un problema. De hecho, esta actividad es fundamental en las
arquitecturas Flux, porque la eliminación de datos de los estados de la store es también la forma en
que eliminamos otras estructuras de datos, como nodos DOM y funciones de manejo de eventos.
Anteriormente en el capítulo, vimos que al vaciar un array, podíamos decir a la vista que eliminara
elementos de la interfaz de usuario. Esencialmente, así es como escalamos las aplicaciones Flux
eliminando los datos que tienen el potencial de causar dolores de cabeza. Imagina una store que
tenía una colección con miles de elementos en ella. Esta colección no sólo es costosa de procesar
como está, sino que también tiene el potencial de crecer mucho más.
Una solución sencilla es vaciar dicha colección cuando no es necesaria. Revisemos esta idea.
Primero, aquí tenemos como se vería la vista:
import React from 'react';
import { hideAll, hideOdd } from '../actions/hide';
// La función de la vista, renderiza un botón
// que borra los datos de la store enviado por
// la acción "HIDE_ALL", y renderiza una lista
// de elementos. El botón ocultar impares borra
// algunos datos de la store enviado por la acción
// "HIDE_ODD".
export default ({ items }) => (
<div>
<button onClick={hideAll}>Hide All</button>
<button onClick={hideOdd}>Hide Odd</button>
<ul>
{items.map(item =>
<li key={item}>{item}</li>
)}
</ul>
</div>
);
Un par de botones y un listado de elementos-sencillo. Cuando un botón es pulsado, llama a la
función creadora de acciones. Pongamos atención a la store ahora:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { HIDE_ALL, HIDE_ODD } from '../actions/hide';
class MyStore extends EventEmitter {
constructor() {
super();
// El estado inicial es una array de "items"
// de 100 números.
this.state = {
items: new Array(100)
.fill(null)
.map((x, y) => y)
};
this.id = dispatcher.register((e) => {
let { state } = this;
switch(e.type) {
// Cuando la acción "HIDE_ALL" es enviada,
// el estado "items" es reseteada a
// un array vacío.
case HIDE:
state.items = []
this.emit('change', state);
break;
// Cuando la acción "HIDE_ODD" es enviada,
// el estado “items" es filtrado para incluir
// sólo los números pares.
case HIDE_ODD:
state.items = state.items.filter(
x => !(x % 2));
this.emit('change', state);
break;
}
});
}
}
export default new MyStore();
La acción HIDE_ALL simplemente elimina todos los elementos asignando un array vacío. Esto es
exactamente lo que hacemos después de eliminar los datos cuando ya no son necesarios. Este es el
verdadero desafío del escalado, la limpieza de datos que tienen el potencial de ser grandes y
costosos de procesar. La acción HIDE_ODD es una variación que filtra números pares. Por último,
veamos cómo todo esto se junta en main.js:
import React from 'react';
import { render } from 'react-dom';
import myStore from './stores/my-store';
import MyView from './views/my-view';
// El contenedor DOM para la vista React...
var container = document.getElementById('my-view');
// Renderiza el componente funcional "MyView"
// de React.
function renderView(state) {
render(
<MyView
items={state.items}/>,
container
);
}
// Re-renderiza el componente React cuando el estado de la
// store cambia.
myStore.on('change', renderView);
// Realiza el renderizado inicial.
renderView(myStore.state);
Optimización de stores inactivas
Un posible problema de escalado con la configuración que hemos utilizado en el ejemplo anterior es
que la vista en sí misma realice algún cálculo costoso. Por ejemplo, no podemos descartar la
posibilidad de que incluso con un array vacío como la información suministrada a renderizar la
vista tenga algunos problemas de implementación. Esto es problemático en una arquitectura Flux,
porque las acciones siempre se envían a las stores, que a su vez notifican a las vistas que las están
escuchando. Así que es importante que las vistas sean rápidas.
Aquí es donde React encaja muy bien en Flux. Los componentes de React están destinados a ser re-
renderizados, desde el componente raíz hasta los componentes hijos. Es capaz de hacer esto
eficientemente debido al DOM virtual que usa bajo la directiva para calcular los parches que luego
se aplican al DOM real. Esto elimina muchos problemas de rendimiento porque la emisión de
muchas llamadas a la API de DOM es un cuello de botella en el rendimiento. Por otro lado, sería un
poco ingenuo asumir que la store publicará cambios en un componente eficiente de React.
Las stores son responsables de emitir eventos de modificación en el momento adecuado. Por lo
tanto, podríamos determinar dentro de la store que cuando una acción dada es enviada, no hay
necesidad de emitir un evento de cambio. Esto implicaría algún tipo de heurística que determinar
que la vista ya está mostrando la información apropiada dado el estado de la store, y que emitir un
evento de cambio ahora no tendría ningún valor. De este modo, podríamos evitar cualquier
problema de rendimiento en la vista. El problema con este enfoque es que estamos construyendo
complejidad en nuestra store. Probablemente es mejor que emitamos eventos de cambio
consistentemente y nos ocupemos de las vistas que están haciendo las cosas ineficazmente. O si aún
no estamos usando React como capa de vista, quizás este sea un argumento a favor de hacerlo.
En el siguiente capítulo, veremos la implementación de la heurística avanzada de
detección de cambios en los componentes de nuestra vista.

Actualización de datos en store


En este capítulo, has visto cómo eliminar datos de las stores de forma que se escalen bien. Si el
usuario se ha movido de una parte de la interfaz de usuario a otra, entonces es probable que se desee
eliminar cualquier dato en store que ya no sea necesario en esta nueva sección. La idea es que en
lugar de eliminar todos nuestros componentes JavaScript, nos centramos en los datos en nuestras
stores, el aspecto de nuestra aplicación más difícil de escalar. Sin embargo, hay un problema
potencial con este enfoque que debemos considerar.
¿Qué sucede si otra store depende de los datos que acabamos de eliminar? Por ejemplo, el usuario
se encuentra en una página impulsada por el estado de la store A. A continuación, se mueven a otra
página, que es conducida por la store B, que depende de la store A. Pero acabamos de borrar el
estado dentro de la store A-¿no va a ser esto un problema para la store B?
Este no es un caso común - la mayoría de nuestras stores no tendrán ninguna dependencia, y
deberíamos estar seguros de borrar los datos no utilizados. Sin embargo, tenemos que llegar a un
plan de juego para las stores que tienen dependencias. Vamos a repasar un ejemplo y empecemos
con las vistas. Primero, tenemos la vista de un radio button, que es un control simple que permite al
usuario cambiar de una lista de usuarios a una lista de grupos:
import React from 'react';
import { id } from '../util';
import { showUsers, showGroups } from '../actions/show';
// Esta vista React muestra dos radio
// buttons que determinan que lista mostrar.
// Advierte que ambos están usando "map()" aunque
// sea un array de un solo elemento. Esto es
// mantener la lógica en el store y fuera de la vista.
export default ({ users, groups }) => (
<div>
{users.map(user =>
<label key={id.next()}>
{user.label}
<input
type="radio"
name="display"
checked={user.checked}
onChange={showUsers}
/>
</label>
)}
{groups.map(group =>
<label key={id.next()}>
{group.label}
<input
type="radio"
name="display"
checked={group.checked}
onChange={showGroups}
/>
</label>
)}
</div>
);
El evento de cambio para ambos radio buttons está conectado a una función creadora de acciones,
que afecta a la visualización de nuestras otras dos vistas: las veremos a continuación, empezando
por la vista de lista de usuarios:
import React from 'react';
// Una simple vista React que muestra un listado de
// usuarios.
export default ({ users }) => (
<ul>
{users.map(({ name, groupName }) =>
<li key={name}>{name} ({groupName})</li>
)}
</ul>
);
Bastante sencillo, y puedes ver que hay una dependencia de grupo aquí, ya que estamos mostrando
el grupo al que pertenece el usuario. Profundizaremos en esa dependencia momentáneamente, pero
por ahora, veamos la vista de lista de grupos:
import React from 'react';
// Una simple vista React que muestra un listado
// de grupos...
export default ({ groups }) => (
<ul>
{groups.map(group =>
<li key={group}>{group}</li>
)}
</ul>
);
Ahora echemos un vistazo a las stores que manejan estas vistas, comenzando por la store de los
radio button:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { SHOW_USERS, SHOW_GROUPS } from '../actions/show';
class Radio extends EventEmitter {
constructor() {
super();
// Esta store representa los radio buttons para
// mostrar los "users" y "groups". Cada uno
// está representada como un array así que podemos
// coger fácilmente y quitar el botón de
// la vistas vaciando el array.
this.state = {
users: [{
label: 'Users',
checked: true
}],
groups: [{
label: 'Groups',
checked: false
}]
};
this.id = dispatcher.register((e) => {
// Fácil acceso a las propiedades estado que
// necesitamos en este manejador. Mira los dos
// metódos getter más abajo.
let { users, groups } = this;
switch(e.type) {
// Marca "users" mostrándolo como "checked".
case SHOW_USERS:
users.checked = true;
groups.checked = false;
this.emit('change', this.state);
break;
// Marca "groups" mostrándolo como "checked".
case SHOW_GROUPS:
users.checked = false;
groups.checked = true;
this.emit('change', this.state);
break;
}
});
}
// Un atajo de fácil acceso al estado de "users".
get users() {
return this.state.users[0]
}
// Un atajo de fácil acceso al estado de "groups".
get groups() {
return this.state.groups[0]
}
}
export default new Radio();
Puedes ver aquí que estamos usando la técnica del array de un solo elemento una vez más. Por eso
tenemos la llamada map() en la vista que utiliza los datos de esta store. La idea es que para ocultar
uno de estos botones, podemos hacerlo aquí mismo en la store, poniéndolo en la lógica de una
colección vacía, fuera de la vista. Nótese que hemos establecido algunas funciones básicas de getter
para hacer más fácil el manejo de estos arrays de un solo elemento. Ahora vamos a echar un vistazo
a la store de grupos:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { SHOW_GROUPS } from '../actions/show';
class Groups extends EventEmitter {
constructor() {
super();
// El estado por defecto de "_group" es un array de nombres
// de grupo.
this.state = {
_groups: [
'Group 1',
'Group 2'
]
};
// El estado de "groups" es el que actualmente esta siendo usado
// por las vistas y es un array vacío por defecto
// porque no se muestra nada por defecto.
this.state.groups = [];
this.id = dispatcher.register((e) => {
let { state } = this;
switch(e.type) {
// La acción "SHOW_GROUPS" mapeara el
// estado de "_groups" al estado de "groups"
// así la vista tendrá algo que mostrar.
case SHOW_GROUPS:
state.groups = state._groups.map(x => x);
this.emit('change', state);
break;
// Por defecto, el estado de "groups" está vacío,
// lo que limpia la vista de elementos. El
// estado de "_groups", sin embargo, permanece intacto.
default:
state.groups = [];
this.emit('change', state);
break;
}
});
}
}
export default new Groups();
Esta store tiene dos piezas de estado- _groups y groups. Sí, son básicamente lo mismo. La
diferencia es que la vista depende de groups, no de los _groups. La store Groups es capaz de
calcular el estado de los grupos basada en _groups. Esto significa que podemos borrar de forma
segura el estado de groups para actualizar el renderizado de la vista mientras el estado de los
_groups no se toca. Otras stores pueden depender de esta store ahora, sin riesgo de que
desaparezcan datos. Echemos un vistazo a la tienda de los usuarios ahora:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import groups from './groups';
import { SHOW_USERS } from '../actions/show';

class Users extends EventEmitter {


constructor() {
super();
// El estado por defecto de "_users" es
// un array de objetos usuarios con referencia a
// los grupos desde otra store.
this.state = {
_users: [
{ name: 'User 1', group: 1 },
{ name: 'User 2', group: 0 },
{ name: 'User 3', group: 1 }
]
};
// Asigna al array de estado "users", el estado
// que actualmente está siendo usado por las vistas. Mira
// "mapUsers()" más abajo.
this.mapUsers();
this.id = dispatcher.register((e) => {
let { state } = this;
switch(e.type) {
// Si estamos mostrando usuarios, necesitamos "waitFor()"
// la store "groups" porque dependemos de ella.
// Entonces podemos usar "mapUsers()" de nuevo.
case SHOW_USERS:
dispatcher.waitFor([ groups.id ]);
this.mapUsers();
this.emit('change', state);
break;
// La acción por defecto es vaciar
// el estado "users" así que la vista
// borrará los elementos de la UI. Sin embargo, el
// estado "_users" permanece, así otras stores
// que dependan de esta podrán seguir accediendo
// a los datos.
default:
state.users = [];
this.emit('change', state);
break;
}
});
}
// Mapea el estado "_users" al estado de "users".
// La idea es que el array "users" pueda ser
// vaciada al actualizar la vista mostrando el array
// "_users" que permanece intacto para el uso de otras stores.
mapUsers() {
this.state.users = this.state._users.map(user =>
Object.assign({
groupName: groups.state._groups[user.group]
}, user)
);
}
}
export default new Users();
Puedes ver que la store Users puede depender del estado de _groups desde la store Groups
para crear el estado que necesita la vista que lista los usuarios. Esta sigue el mismo patrón que la
tienda Groups en el sentido de que tiene un estado _users y un estado users. Esto permite que
otras vistas dependan de _users si es necesario, y nosotros todavía podemos borrar el estado de
users para borrar la interfaz de usuario. Sin embargo, si resulta que no hay nada dependiendo de
esta store, podemos revertir el patrón para que sólo haya una pieza que se elimina cuando la vista
actual ya no la requiera. Por último, miremos el módulo main.js y veamos cómo encaja todo
esto:
import React from 'react';
import { render } from 'react-dom';
import radio from './stores/radio';
import users from './stores/users';
import groups from './stores/groups';
import Radio from './views/radio';
import Users from './views/users';
import Groups from './views/groups';
// El elemento DOM contenedor...
var container = document.getElementById('app');
// Renderiza los componentes React. El estado para
// las stores Flux es pasado como props.
function renderApp(
radioState=radio.state,
usersState=users.state,
groupsState=groups.state
){
render(
<div>
<Radio
users={radioState.users}
groups={radioState.groups}/>
<Users
users={usersState.users}/>
<Groups
groups={groupsState.groups}/>
</div>,
container
);
}
// Renderiza la aplicación con el nuevo estado de "radio".
radio.on('change', (state) => {
renderApp(state, users.state, groups.state);
});
// Renderiza la aplicaicón con el nuevo estado de "users".
users.on('change', (state) => {
renderApp(radio.state, state, groups.state);
});
// Renderiza la aplicación con el nuevo estado de “groups".
groups.on('change', (state) => {
renderApp(radio.state, users.state, state);
});
// Renderizado inicial de la aplicación...
renderApp();

Sumario
El enfoque de escalar una arquitectura Flux está en la información que las stores producen, en lugar
de los diversos componentes. Este capítulo comenzó con una discusión sobre las prácticas comunes
de otras arquitecturas que implican la constante creación y destrucción de componentes JavaScript.
Esto se hace para liberar recursos, pero viene a un costo - el potencial de error. A continuación,
observamos la naturaleza relativamente estática de las arquitecturas de Flux, donde los
componentes tienen una vida útil larga. No tiene que estar constantemente creando y destruyendo
componentes, lo que significa que hay menos posibilidades de problemas.
A continuación, cubrimos el concepto de escalar la información. Lo hicimos demostrando que
nuestros componentes JavaScript y las conexiones entre ellos eran las menores de nuestras
preocupaciones cuando se trata de escalar la arquitectura. El verdadero desafío viene cuando hay
muchos datos que procesar, y es probable que los datos que entran en el sistema crezcan mucho más
rápido que el número de componentes JavaScript que tenemos.
Cerramos el capítulo con algunos ejemplos de cómo tratar los datos no utilizados de la store. Este es
en última instancia el aspecto más importante de escalar una arquitectura Flux ya que devuelve al
navegador los recursos no utilizados. En el próximo capítulo, abordaremos el tema de stores
inmutables. Esto es algo a lo que hemos aludido a lo largo del libro, y sobre lo que concentraremos
un poco la atención ahora.
9
Stores inmutables
En este capítulo, vamos a ver datos inmutables en las stores Flux. Inmutabilidad es un término que a
menudo coincide con la programación funcional. Los datos inmutables son datos que no cambian
(mutan) una vez creados. La principal ventaja es que pueden predecir la causa raíz de los cambios
de datos en una aplicación porque los datos no pueden ser cambiados inadvertidamente por efectos
secundarios. Inmutabilidad y Flux se llevan bien. porque ambos son explícitos y predecibles.
Empezaremos hablando de actualizaciones ocultas o efectos secundarios. Flus por sí mismo
desaconseja tales cosas y los datos inmutables ayudan a hacer cumplir la idea. Entonces, vamos a
repasar lo que estos efectos secundarios implican para la integridad de nuestra arquitectura Flux.
Las consecuencias más graves de los efectos secundarios causados por la mutación de los datos de
store son las consiguientes interrupciones en el flujo de datos unidireccional de Flux. A
continuación, veremos los costos de inmutabilidad, los cuales están relacionados en su mayoría con
los recursos adicionales requeridos, lo que puede llevar a una degradación notable del rendimiento.
Por último, vamos a ver el librería Immutable.js para ayudar a realizar transformaciones en
datos inmutables.

Renunciar a actualizaciones ocultas


La naturaleza unidireccional de Flux es lo que lo diferencia de otras arquitecturas frontend
modernas. La razón por la que el flujo de datos unidireccional funciona es porque la acción es la
única manera en que los nuevos datos pueden entrar en el sistema. Sin embargo, esto no es
estrictamente ejecutado por Flux, y esto significa que algún pedazo de código errante tiene el
potencial para romper completamente nuestra arquitectura.
En esta sección veremos cómo algo así es posible incluso en Flux. A continuación, veremos cómo
las vistas obtienen normalmente sus datos de las stores y si hay o no una mejor manera. Finalmente,
pensaremos en otros componentes de Flux y veremos si se puede hacer algo más que almacenar
datos inmutables.

Cómo romper el Flux


La manera más fácil de romper Flux es mutando el estado de una store sin ir a través de los canales
apropiados. El dispatcher es la puerta de entrada para nuevos datos que entra en el sistema, y
también coordina a los manejadores de acciones de nuestras stores. Por ejemplo, una acción podría
activar el manejador de un par de stores, usando la el payload de acción de diferentes maneras. Esto
simplemente no sucederá si el estado de las stores está siendo mutado directamente. Podríamos
tener suerte y que los cambios que hacemos no tengan ningún efecto colateral. ¿Pero no es la
premisa de ser explícito con acciones que no podemos predecir los efectos secundarios complejos?
Si traspasamos la frontera y empezamos a manipular directamente el estado aquí y allá, ¿qué evitará
que hagamos esto más a menudo? El escenario más probable es una vista que muta los datos de la
store. Esto se debe a que las vistas típicamente tienen referencias a stores, mientras que otros
componentes de Flux típicamente no lo hacen. Así que cuando el el usuario hace clic en un botón y
nuestro manejador simplemente cambia el estado de una store en lugar de enviar una acción,
podríamos encontrarnos en problemas.
Veamos un ejemplo que pone de relieve lo peligroso que es operar en el exterior del campo de juego
de Flux. Primero examinemos la store de botón:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { TOGGLE } from '../actions/toggle';
class Button extends EventEmitter {
constructor() {
super();
// El estado por defecto is mostrar el botón
// como habilitado y proceder a los eventos click.
this.state = {
text: 'Enabled',
disabled: false
};
this.id = dispatcher.register((e) => {
let { state } = this;
switch(e.type) {
// Cuando la acción “TOGGLE” es enviada,
// el siguiente estado del botón depende del
// actual estado de la propiedad
// “disabled”
case TOGGLE:
state.text = state.disabled ?
'Enabled' : 'Disabled';
state.disabled = !state.disabled;
this.emit('change', state);
break;
}
});
}
}
export default new Button();
Parece bastante sencillo: controla el texto y el estado de disabled de los botones. Esto es
bastante simple, pero sólo si cumplimos con las reglas de Flux y enviamos acciones para cambiar el
estado de una store. Ahora, echemos un vistazo a un componente de vista que utiliza esta store para
renderizarse:
import React from 'react';
import button from '../stores/button';
import { toggle } from '../actions/toggle';
function onClick() {
// ¡Oh! Esto rompe totalmente Flux...
button.state.disabled = !button.state.disabled;
// Llama al creador de acciones como deberíamos...
toggle();
}
// Renderiza tu típico botón HTML, completa
// con propiedades y un manejador callback para
// eventos click.
export default ({ text, disabled }) => (
<button
onClick={onClick}
disabled={disabled}>{text}</button>
);
Lo que se supone que debe ocurrir aquí es que el botón se deshabilite al hacer clic en él, porque el
store de botón cambiará el estado en consecuencia cuando se envíe la acción TOGGLE. Esto
funciona como se esperaba. Sin embargo, el resultado es que esto nunca funcionará como se
esperaba, debido a una línea encima de la llamada a toggle() . Aquí, estamos manipulando
directamente el estado de una store. Esto evita que el comportamiento esperado se produzca cuando
se envía la acción TOGGLE, porque el estado ya ha cambiado, así que ahora volverá a cambiar.
Son estos pequeños hacks los que pueden causar grandes problemas si no tenemos cuidado. Cuando
miras este módulo de vista, el código problemático salta a la vista. Imagínate un proyecto real con
muchas más vistas que son cada una mucho más grande que ésta: ¿serías capaz de detectar este
problema antes de que sea demasiado tarde?

Obtención de datos de un store


Dado que referenciar el estado de la store es algo peligroso, ¿podríamos evitarlo por completo? Esto
reduciría drásticamente el potencial de errores, como vimos en la sección anterior. Por ejemplo,
cuando dos stores dependen la una de la otra, utilizan el método waitFor() del dispatcher para
asegurarse de que la store de la que dependemos se actualice primero. Entonces podemos acceder
directamente a la store, sabiendo que su estado ya ha sido actualizado. El enfoque se visualiza de la
siguiente manera:

La store dependiente hace referencia directamente al estado de la store de la que depende, que es
algo que puede llevar a grandes problemas si no tenemos cuidado. Una alternativa sería hacer que la
store dependiente escuchara el evento de modificación en el de la que depende. El callback puede
utilizar el nuevo estado que se le ha pasado como un argumento. Por supuesto, todavía tendríamos
que usar waitFor() o algo por el estilo para asegurarnos de que las stores se actualizan en el
orden correcto. Este enfoque es el que se muestra aquí:
Esto está empezando a parecerse a un componente vista - las vistas escuchan el evento de cambio
para que puedan generar actualizaciones en interfaz de usuario que reflejen los cambios de estado.
Las vistas también tienen la necesidad de realizar una representación inicial de los datos de la store,
y esta es la razón por la que normalmente tienen el estado de la store de referencia. El problema con
cualquiera de estas ideas es que ninguna de ellas en realidad nos aíslan del acceso directo al estado
de la store-quién sabe qué tipo de referencia se pasará como uno de estos argumentos de callback.
El otro problema es que mediante la introducción de funciones callback donde es posible la lectura
directa de un valor es una sobrecomplicación en términos de diseño. Tiene que haber una manera
mejor. Haciendo que nuestros datos de store sean inmutables es un paso en la dirección correcta.
En el próximo capítulo, vamos a implementar nuestro propio componente
dispatcher. Mientras lo hacemos, pensaremos en implementar algunas medidas de
seguridad contra el acceso a los datos de estado de una store mientras se está
llevando a cabo una ronda de actualización, pero la store no ha sido actualizada.
Esto facilitará la resolución de problemas con las dependencias.

Todo es inmutable
Como último tema antes de discutir cómo podríamos hacer cumplir la inmutabilidad, hablemos de
la idea de que todo en una arquitectura Flux es inmutable. Teóricamente, esto no debería ser tan
difícil de hacer ya que el estado desacordonado de Flux no vive en cualquier otro lugar que no sea
dentro de una store. Entonces, empecemos con las stores.
¿Deberían todas nuestras stores ser inmutables, o quizás sólo algunas de ellas? Tener sólo unas
pocas stores inmutables no es una buena idea porque promueve la inconsistencia. ¿Qué hay sobre
tener inmutabilidad total?, ¿es necesario? Ahora bien, esta es una importante pregunta que uno tiene
que hacer acerca de su arquitectura porque no hay una respuesta definitiva. El argumento de la
inmutabilidad funciona cuando necesitamos esa garantía adicional de que no habrá sorpresas con
los estados de las stores más adelante. El contraargumento es que somos lo suficientemente
disciplinados como programadores como para que los mecanismos de inmutabilidad sólo añadan
sobrecostes.
Pasaremos el resto de este capítulo argumentando a favor de datos inmutables, simplemente porque
lo positivo supera a los negativo en casi todos los casos. Independientemente de cómo pienses
acerca de la inmutabilidad, es bueno saber cuáles son sus fortalezas en una arquitectura Flux,
incluso si no vas a usarla.
¿Qué hay de los componentes de la vista? ¿Pueden ser realmente inmutables? Bueno, resulta que
que no pueden, porque la API de DOM no lo permite. Nuestros componentes de vista tienen que
manipular el estado de los elementos de la página. Sin embargo, si estamos usando una tecnología
de visualización como React, entonces obtenemos un velo de inmutabilidad porque la idea es
siempre volver a renderizar componentes. Así que parece como si estuviéramos eliminando a los
viejos y reemplazandolos por otros nuevos, mientras que, React resuelve el problema de la
manipulación del DOM por nosotros. Esto promueve la idea de que el estado no tiene lugar dentro
del flujo de una vista.

Imponer el flujo de datos unidireccional


Si los nuevos datos sólo entran en el sistema a través de payloads de acción entregadas por el
dispatcher y los datos de nuestra tienda son inmutables, tenemos un flujo de datos unidireccional.
Este es el objetivo, así que la pregunta es, ¿cómo hacemos cumplir esto? ¿Podemos decir
simplemente que los datos de nuestra store son inmutable y terminar con esto? Bueno, eso es algo
por lo que alegrarse, absolutamente, pero hay más que eso.
En esta sección, trataremos el concepto de flujo de datos en direcciones no deseadas, y qué causa
que esto suceda. Entonces consideraremos la noción de tener demasiadas stores y no realiza
suficientes acciones que contribuyen a flujos de datos disfuncionales. Finalmente, examinaremos
algunas técnicas que podemos utilizar para hacer que los datos de nuestra store sean inmutables.

Flujo de datos hacia atrás, hacia los lados y con fugas


Las arquitecturas de Flux tienen un flujo de datos unidireccional - los datos entran por la izquierda y
por la derecha. Es fácil de visualizar como un flujo que avanza. ¿Cuales son algunas de las formas
en que esto puede salir mal? Tomemos el flujo hacia atrás, por ejemplo. Si una instancia de vista
contiene una referencia a una instancia de store y procede a mutar su estado, entonces el flujo se
mueve de la vista a la store. Esto es todo lo contrario de la dirección de flujo esperada, por lo que se
está moviendo hacia atrás. He aquí una ilustración de como luce esto:

Esto obviamente no es lo que esperaríamos cuando trabajamos con un sistema Flux. Pero también
es un escenario probable a menos que descartemos la posibilidad de que el estado de la store
devuelva estructuras de datos inmutables a cualquier otro componente que quiera interactuar con
ella. ¿Qué pasa con las stores? ¿Pueden mutar el estado de otra store? No deberían, y si lo hacen,
parecería un flujo de datos lateral.
ProTip: Todo lo que va de lado es malo.
Aquí tenemos como se vería un flujo de datos lateral entre dos stores Flux:

Esto es tan malo como el componente de vista que muta directamente el estado de una store, porque
el estado que acabamos de cambiar podría afectar al siguiente estado calculado. Este es la misma
situación que vimos en el primer ejemplo de código que vimos en el capítulo.
¿Qué hay de las acciones? ¿Son capaces de manipular directamente el estado de una store? Este es
probablemente el escenario menos probable, porque se supone que las funciones creadoras de
acciones envían acciones después de coordinar cualquier comportamiento asíncrono. Sin embargo,
una función creadora de acciones podría mutar incorrectamente un estado de una store en una
callback del manejador AJAX, por ejemplo. Esto es a lo que nos referimos como fuga de flujos
porque van a alrededor del dispatcher. Así que, estamos filtrando mutaciones sin ninguna acción
rastreable que muestre de dónde se originaron. He aquí una ilustración de la idea:

¿Demasiadas stores?
Siempre existe la posibilidad de que haya demasiadas stores Flux en nuestra arquitectura. Quizás la
aplicación ha crecido más allá de lo que habíamos diseñado originalmente en términos de
funcionalidades. Ahora, simplemente asignar una store a una funcionalidad no será suficiente
porque hay docenas de stores.
Si no somos capaces de controlar el conteo de stores, un posible resultado es una mutación de
estado más directa por otros componentes. Es sólo una cuestión de conveniencia, si hay una
tonelada de stores en las que pensar, significa que vamos a tener que encargarnos de varias otras
actividades de desarrollo relacionadas con el dispatcher siempre que queramos hacer algo. Cuando
hay muchas stores, hay la necesidad de manipular su estado directamente. Eliminar stores reduce
este impulso.

No hay suficientes acciones


¿Es posible que nuestra arquitectura Flux no tenga suficientes acciones? Por ejemplo, una vista en
la que estamos trabajando necesita cambiar el estado de una store. No hay acción para manejar esto
por nosotros, así que en lugar de construir un nuevo creador de acciones y actualizar la store para
manejar la lógica, mutamos directamente la store. Suena como una tarea bastante fácil-construir una
función creadora de acción y añadir la lógica necesaria para la modificación de store. Pero si
tenemos que seguir implementando estas funciones creadoras de acciones únicas, eventualmente
dejaremos de preocuparnos. Hay dos maneras de solucionar este problema. La primera es
implementar acciones más genéricas que se apliquen a más de una situación específica y puedan
aceptar parámetros. La segunda es construir un puñado de funciones creadoras de acciones que son
relevantes para la funcionalidad en la que se está trabajando, incluso antes de que las necesites.
Cuando sabes que las funciones están ahí, en el fondo de tu mente, es más probable que las uses.

Hacer cumplir la inmutabilidad


Exploremos algunos enfoques diferentes para mantener inmutable el estado de la store. El objetivo
es que cuando alguna entidad externa hace referencia al estado de una store, cualquier cambio que
esa entidad haga que el estado no afecte realmente a la store porque los datos son inmutables.
Empezaremos por implementar una store que no devuelva una referencia a su estado, sino que
devuelva una copia usando Object.assign():
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { MY_ACTION } from '../actions/my-action';
// El estado de esta store está encapsulado
// en el módulo.
var state = {
first: 1,
second: 2,
};
class Copy extends EventEmitter {
constructor() {
super();
this.id = dispatcher.register((e) => {
switch(e.type) {
case MY_ACTION:
// Campia "state" con nuevas propiedades…
Object.assign(state, e.payload);
this.emit('change', state);
break;
}
});
}
// Devuelve una copia de "state", no una referencia
// al original.
get state() {
return Object.assign({}, state);
}
}
export default new Copy();
Aquí, puedes ver que el estado real de la store está en una variable de estado a nivel de módulo.
Esto significa que no es accesible directamente por desde exterior porque no se exporta. Queremos
que el estado esté encapsulado así para que sea más difícil para otros componentes mutarlo. Si otros
componentes necesitan acceso de lectura a las propiedades de estado de la store, éstos puede leer la
propiedad de estado de la store. Ya que este es un método getter, puede calcular el valor que será
devuelto. En este caso, crearemos un nuevo objeto sobre la marcha. Ahora veamos una store que
almacena su estado en una constante:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { MY_ACTION } from '../actions/my-action';
// El estado de esta store está encapsulado
// en el módulo. Es almacenado como
// una constante.
const state = {
first: 1,
second: 2,
};
class Constant extends EventEmitter {
constructor() {
super();
this.id = dispatcher.register((e) => {
switch(e.type) {
case MY_ACTION:
// Muta "state" con nuevas propiedades...
Object.assign(state, e.payload);
this.emit('change', state);
break;
}
});
}
// Devuelve una referencia a la constante "state"...
get state() {
return state;
}
}
export default new Constant();
Esta store tiene la misma estructura y los mismos patrones que la store Copy. La diferencia es que
ese estado no es una variable, es una constante. Esto significa que no deberíamos poder mutarlo,
¿verdad? Bueno, no del todo, sólo que no podemos asignarle nuevos valores. Así que este tiene un
valor limitado porque el getter state() devuelve una referencia directa a la constante. Veremos
cómo funciona esto momentáneamente, cuando otros componentes usen la store. Veamos un
enfoque más, que usa Object.frozen() para hacer objetos inmutables:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { MY_ACTION } from '../actions/my-action';
// El estado está encapsulado en
// este módulo...
var state;
// Une nuevos valores con los valores actuales, congela
// el nuevo estado, y se lo asigna a "state".
function change(newState) {
let changedState = Object.assign({}, state, newState);
state = Object.freeze(changedState);
}
// Asigna el estado inicial y lo congela...
change({
first: 1,
second: 2,
});
class Frozen extends EventEmitter {
constructor() {
super();
this.id = dispatcher.register((e) => {
switch(e.type) {
case MY_ACTION:
// Llama a "change()" para modificar el valor
// de "state" y lo vuelve a congelar.
change(e.payload);
this.emit('change', state);
break;
}
});
}
// Devuelve una referencia a "state" congelado...
get state() {
return state;
}
}
export default new Frozen();
El getter state() está devolviendo una referencia a la variable de estado congelado. Lo
interesante de este enfoque es que no necesariamente necesitamos hacer una nueva copia de los
datos porque nuestra función change() los ha hecho inmutable. Y cuando la propia store
necesita actualizar su estado, es cuando el estado se vuelve a congelar.
Veamos una comparación de estas aproximaciones. Primero, impotamos las stores y obtenemos la
referencia de sus estados:
import copy from './stores/copy';
import constant from './stores/constant';
import frozen from './stores/frozen';
var copyState = copy.state;
var constantState = constant.state;
var frozenState = frozen.state;
copyState.second++;
constantState.second++;
try {
frozenState.second++;
} catch (err) {
console.error(err);
// →
// TypeError: Cannot assign to read only property
// 'second' of object
}
console.assert(
copy.state.second !== copyState.second,
'copy.second mutated'
);
console.assert(
constant.state.second !== constantState.second,
'constant.second mutated'
);
// → Assertion failed: constant.second mutated
Parece que fuimos capaces de cambiar con éxito el estado de copyState. Esto es una especie de
de verdad-hemos cambiado el estado de una copia que en realidad no refleja el estado de la store. El
cambio constantState, por otro lado, tiene efectos secundarios porque cualquier otro
componente que lea el estado de la store constante verá este cambio.
Cuando intentamos cambiar frozenState, se lanza un TypeError. Este podría ser el resultado
deseado, ya que se ha hecho explícito que lo que estamos tratando de hacer con fronzenState
no está permitido. Cosas similares suceden cuando añadimos nuevas propiedades al estado de la
store-copy falla silenciosamente, constant falla, y frozen falla explícitamente:
copyState.third = 3;
constantState.third = 3;
try {
frozenState.third = 3;
} catch (err) {
console.error(err);
// →
// TypeError: Can't add property third, object is
// not extensible
}
Finalmente, veamos los datos de estado que son enviados cuando el evento de cambio es emitido:
copy.on('change', (state) => {
console.assert(state !== copyState, 'copy same');
console.assert(state.fourth, 'copy missing fourth');
});
constant.on('change', (state) => {
console.assert(state !== constantState, 'constant same');
// → Assertion failed: constant same
console.assert(state.fourth, 'constant missing fourth');
});
frozen.on('change', (state) => {
console.assert(state !== frozenState, 'frozen same');
console.assert(state.fourth, 'frozen missing fourth');
});
myAction({ fourth: 4 });
La función myAction() ampliará el estado de la store con nuevos datos. Como podemos ver una
vez más, el enfoque constant nos ha fallado porque devuelve la misma referencia que fue mutada.
En términos generales, ninguno de estos enfoques es particularmente fácil de implementar en la
práctica. Esta es otra razón por la que queremos considerar seriamente el uso de una librería como
Immutable.js , donde la inmutabilidad es el modo por defecto y mayormente oculto a nuestro
código.

El coste de los datos inmutables


A estas alturas ya es consciente de la ventaja que los datos inmutables aportan a la arquitectura
Flux: un nivel de seguridad sobre nuestro flujo de datos unidireccional. Esta red de seguridad tiene
un costo. En esta sección, discutiremos lo costosa que puede ser la inmutabilidad y lo que se puede
hacer al respecto.
Comenzaremos cubriendo el mayor problema de la inmutabilidad: la asignación de memoria
transitoria y la recolección de basura. Estas cosas son grandes amenazas para el rendimiento de
nuestra arquitectura Flux. A continuación, pensaremos en reducir la cantidad de asignaciones de
memoria mediante la agrupación de transformaciones en datos inmutables. Finalmente, pensaremos
en las formas en que los datos inmutables eliminan el código que sólo es necesario para manejar
escenarios en los que los datos son mutables.

La recolección de basura es cara


Una cosa buena acerca de las estructuras de datos mutables es que una vez que se asignan, tienden a
quedarse por un tiempo. Es decir, no necesitamos copiar las propiedades de una estructura existente
en una nueva, luego destruir la vieja cada vez que necesitemos hacer una actualización. Esto es
software, así que vamos a hacer muchas actualizaciones.
Con datos inmutables, nos enfrentamos a un reto de consumo de memoria. Cada vez que muta un
objeto, tenemos que asignar una nueva copia de ese objeto. Imagínate que estamos en una haciendo
cambios a objetos inmutables en una colección-esto se suma muchas asignaciones de memoria en
un corto período de tiempo, un buen montón si se quiere. Además, los objetos viejos que han sido
reemplazados por los nuevos no son instantáneamente borrados de la memoria. Tienen que esperar a
que el recolector de basura los limpie.
Cuando nuestra aplicación utiliza más memoria de la que necesita, el rendimiento se ve afectado.
Cuando el recolector de basura tiene que funcionar con frecuencia debido a nuestras asignaciones
de memoria, el rendimiento se ve afectado. Es el recolector de basura más que cualquier otra cosa.
que desencadena experiencias de usuario lentas, porque nuestro código JavaScript no puede
responder a cualquier evento pendiente mientras se está ejecutando.
Tal vez hay un enfoque de los datos inmutables que es menos intensivo en memoria que el
reemplazo de objetos grandes cuando todo lo que queremos es una simple actualización.

Mutaciones por lotes


Por suerte para nosotros, las stores mutan su propio estado. Esto significa que las callbacks de los
dispatchers de la store encapsulan todo lo que sucede durante las transformaciones de estado. Así
que si nuestras stores tienen datos de estado inmutables, entonces el mundo exterior no necesita
saber acerca de los atajos que la store toma internamente para reducir el número de asignaciones de
memoria.
Digamos que una store recibe una acción y tiene que realizar tres transformaciones separadas sobre
su estado: hacer una transformación que resulte en un nuevo objeto, hacer otra transformación sobre
ese nuevo objeto, y así sucesivamente. Se trata de una gran cantidad de asignaciones de memoria
transitoria para datos intermedios que ningún otro componente tocará jamás. Aquí hay una
ilustración de lo que está pasando:

Queremos que el resultado final sea una nueva referencia de estado, pero el nuevo estado
intermediario que se crea en el medio es un despilfarro. Veamos si hay una manera de que podamos
agrupar estas transformaciones de estado antes de que se devuelva el valor final inmutable:

Ahora, sólo estamos asignando un nuevo objeto, a pesar de hacer tres transformaciones de estado.
Las mutaciones que estamos haciendo dentro de una store Flux son absolutamente intrascendentes
para cualquier otro componente del sistema, pero mantenemos inmutabilidad para cualquier otra
cosa que quiera acceder y leer este estado.

Compensación del coste


La parte dolorosa de los datos mutables es que los componentes que utilizan estos datos tienen que
tenerlos en cuenta para los efectos secundarios. No saben necesariamente cuándo o cómo estos
datos mutarán. Así que necesitan un código de manejo de efectos secundarios. Mientras que el
código que maneja los inesperados efectos secundarios a menudo no utiliza más memoria, no es
libre de ejecución. Cuando hay para manejar casos límite en toda nuestra fuente, la degradación del
rendimiento aumentar. Con datos inmutables, podemos eliminar la mayoría, si no todo, este código
extraño que comprueba el estado de algo, porque podemos predecir mejor lo que va a pasar. Esto
ayuda a compensar el costo de las asignaciones adicionales de memoria y la ejecución del
recolector de basura. Incluso si no estamos utilizando datos inmutables en nuestras stores, las
arquitecturas de Flux hacen que la necesidad de un código de manipulación de efectos secundarios
esté prácticamente obsoleto. Un flujo de datos unidireccional hace que Flux sea muy predecible.

Usando Immutable.js
La biblioteca Immutable.js de Facebook proporciona estructuras de datos JavaScript
inmutables. Esto puede sonar trivial pero hay mucho que pasa entre bastidores para hacer que esto
funcione, es decir, crear nuevas instancias a partir de transformaciones con la misma eficiencia
como sea posible.
En esta sección, veremos listas y mapas inmutables. Estos son sustitutos viables para arrays y
objetos planos, respectivamente, en nuestros flujos de datos de stores. Entonces, indagaremos en
cómo Immutable.js puede componer transformaciones complejas sin la necesidad de
representaciones intermediarias. Finalmente, veremos cómo Immutable.js devuelve lo misma
instanca cuando no hay mutaciones después de pasar por una transformación, permitiendo la
detección de cambios eficientes.

Listas y mapas inmutables


Empezaremos por ver las listas y mapas, ya que son estructuras bastante comunes que
necesitaremos implementar en nuestras stores. Las listas son como arrays y los mapas son como
objetos JavaScript simples. Vamos a implementar una store que usa una lista:
import { EventEmitter } from 'events';
import Immutable from 'Immutable';
import dispatcher from '../dispatcher';
import { LIST_PUSH, LIST_CAPS } from '../actions/list';
// El estado de esta tienda es una instancia
// "Immutable.List"...
var state = Immutable.List();
class List extends EventEmitter {
constructor() {
super();
this.id = dispatcher.register((e) => {
switch(e.type) {
// Cuando la acción "LIST_PUSH" es enviada,
// creamos una nueva instancia List llamado
// "push()". La nueva lista es asignada a "state".
case LIST_PUSH:
this.emit('change',
(state = state.push(...e.payload)));
break;
// Cuando la acción "LIST_CAPS" es enviada,
// creamos una nueva instancia List llamado
// "map()". La nueva lista es asignada a "state".
case LIST_CAPS:
this.emit('change',
(state = state.map(x => x.toUpperCase())));
break;
}
});
}
get state() {
return state;
}
}
export default new List();
Puede ver que la variable de estado inicializa una instancia Immutable.List() vacía (la
palabra clave new no es necesaria porque son funciones que devuelven nuevas instancias). Cada
vez que llamamos a un método en esta instancia de la lista, se devuelve una nueva instancia. Por eso
tenemos que asignar el resultado de llamar a push() y map() a state .
Ahora vamos a implementar un store de mapeo:
import { EventEmitter } from 'events';
import Immutable from 'Immutable';
import dispatcher from '../dispatcher';
import { MAP_MERGE, MAP_INCR } from '../actions/map';
// El estado de este store es una instancia
// "Immutable.Map"…
var state = Immutable.Map();
class MapStore extends EventEmitter {
constructor() {
super();
this.id = dispatcher.register((e) => {
switch(e.type) {
// Cuando la acción “MAP_MERGE" es enviada,
// creamos una nueva instancia Map llamando a
// "merge()". El nuevo mapa es asignado a "state".
case MAP_MERGE:
this.emit('change',
(state = state.merge(e.payload)));
break;
// Cuando la acción "MAP_INCR" es enviada,
// creamos una nueva instancia Map llamando a
// "map()". El nuevo mapa es asignado a "state".
case MAP_INCR:
this.emit('change',
(state = state.map(x => x + 1)));
}
});
}
get state() {
return state;
}
}
export default new MapStore();
Como puedes ver, los mapas siguen los mismos patrones de inmutabilidad que las listas. La
principal diferencia es que están indexados por clave en lugar de por indice. Ahora veamos cómo se
usan estas dos stores:
import list from './stores/list';
import map from './stores/map';
import { listPush, listCaps } from './actions/list';
import { mapMerge, mapIncr } from './actions/map';

// Registra los objetos en la store "list" cuando


// su estado cambia.
list.on('change', (state) => {
for (let item of state) {
console.log(' list item', item);
}
});
// Registra los objetos en la store "map" cuando
// su estado cambia.
map.on('change', (state) => {
for (let [key, item] of state) {
console.log(` ${key}`, item);
}
});
console.log('List push...');
listPush('First', 'Second', 'Third');
// → List push...
// list item First
// list item Second
// list item Third
console.log('List caps...');
listCaps();
// → List caps...
// list item FIRST
// list item SECOND
// list item THIRD
console.log('Map merge...');
mapMerge({ first: 1, second: 2 });
// → Map merge...
// first 1
// second 2
console.log('Map increment...');
mapIncr();
// → Map increment...
// first 2
// second 3

Transformaciones inmutables
Ahora, es el momento de implementar una transformación más involucrada dentro de una store
Flux. Esto significa encadenar operaciones en estructuras Immutable.js para crear una nueva
estructura. Pero, ¿qué pasa con la asignación de memoria intermedia? He aquí una store que intenta
utilizar menos memoria mientras realiza transformaciones en el estado de una store:
import { EventEmitter } from 'events';
import Immutable from 'Immutable';
import dispatcher from '../dispatcher';
import { SORT_NAMES } from '../actions/sort-names';
// El estado es un objeto con dos instancias
// list inmutables. La primera es una lista que mapea
// usuarios. La segunda es una lista de nombres de usuario y
// está vacía por defecto.
const state = {
users: Immutable.List([
Immutable.Map({ id: 33, name: 'tHiRd' }),
Immutable.Map({ id: 22, name: 'sEcoNd' }),
Immutable.Map({ id: 11, name: 'firsT' })
]),
names: Immutable.List()
};
class Users extends EventEmitter {
constructor() {
super();
this.id = dispatcher.register((e) => {
switch(e.type) {
// La acción "SORT_NAMES" es enviada...
case SORT_NAMES:
// Determina el multiplicador "sort" que le es pasado
// a “sortBy()" para ordenar en dirección ascendente
// o descendente.
let sort = e.payload === 'desc' ? -1 : 1;
// Asigan la lista ordenada de "users" después
// de realizar una serie de transformaciones. Las
// llamada "toSeq()" y"toList()" no son estricamente
// necesarias. Cualquier llamada entre ellas, sin embargo,
// no resulta en una nueva estructura creada.
state.names = state.users
.sortBy(x => x.get('id') * sort)
.toSeq()
.map(x => x.get('name'))
.map(x => `${x[0].toUpperCase()}${x.slice(1)}`)
.map(x => `${x[0]}${x.slice(1).toLowerCase()}`)
.toList();
this.emit('change', state);
break;
}
});
}
get state() {
return state;
}
}
export default new Users();
La acción SORT_NAMES da como resultado algunas transformaciones interesantes que le están
ocurriendo a nuestra lista inmutable. La idea es asignarlo a una lista de nombres de usuario en
mayúsculas, ordenados por id usuario. La técnica que se emplea aquí consiste en convertir la lista
una vez ordenada, usando toSeq() . Esto se hace para evitar que las llamadas map() llamen
desde nuevas estructuras, porque en realidad no necesitamos una estructura concreta hasta que
terminemos de hacer el mapeo. Para ello sólo tenemos que llamar a toList() , que llamará a
todos los mapeos que hemos establecido en la secuencia y crea la lista. Esto significa que las únicas
estructuras que estamos creando aquí, son la nueva lista de sortBy(), la nueva desde toSeq() ,
y la nueva lista desde toList() .
En este ejemplo en particular, esto podría ser excesivo, simplemente debido al hecho de que hay
tres operaciones realizadas en una lista de tres elementos. Así que, simplemente eliminaríamos
toSeq() y toList() de nuestro código para simplificar las cosas. Sin embargo, a medida que
aumentamos las colecciones a mayor escala y realizamos transformaciones más complejas en ellas,
no hace daño saber sobre esta técnica para reducir la huella de memoria de nuestra arquitectura.
Veamos esta store en acción ahora:
import users from './stores/users';
import { sortNames } from './actions/sort-names';
// Registra los nombres de usuario...
users.on('change', ({names}) => {
for (let item of names) {
console.log(' name', item);
}
});
console.log('Ascending...');
sortNames();
// → Ascending...
// name First
// name Second
// name Third
console.log('Descending...');
sortNames(true);
// → Descending...
// name Third
// name Second
// name First

Detección de cambios
En este último ejemplo del capítulo, veremos si podemos usar Immutable.js para implementar
una eficiente detección de cambios en nuestras stores Flux. En realidad, la detección en sí misma
tendrá lugar en la vista React, pero esto depende del estado de la store usando un objeto
Immutable.js. ¿Por qué querríamos hacer esto? ¿No es React ya suficientemente eficiente en la
computación de diferencias utilizando su DOM virtual? React definitivamente sobresale aquí, pero
todavía tiene que hacer una buena cantidad de trabajo para darse cuenta de que no es necesario
volver a renderizar. Podemos echar una mano a nuestros componentes de React dando pistas sobre
que el estado de la store no ha cambiado. Así que sin más preámbulos, aquí está la store que
usaremos:
import { EventEmitter } from 'events';
import Immutable from 'Immutable';
import dispatcher from '../dispatcher';
import { MY_ACTION } from '../actions/my-action';
// El estado de la store es una instacia Immutable.js Map.
var state = Immutable.Map({
text: 'off'
});
class MyStore extends EventEmitter {
constructor() {
super();
this.id = dispatcher.register((e) => {
switch(e.type) {
// Cuando "MY_ACTION" es enviada, asignamos
// la propiedad "text" como "state" así como
// "payload". Si el valor ha cambiado, "state"
// "set()" devuelve una nueva instancia. Si no
// hay cambios, devuelve la misma instancia.
case MY_ACTION:
this.emit('change',
(state = state.set('text', e.payload)));
break;
}
});
}
get state() {
return state;
}
}
export default new MyStore();
Nada elegante se hace aquí; simplemente estamos usando un Map de Immutable.js como
nuestro estado de store. A continuación, asignaremos a la nueva instancia de Map el estado cuando
se llame a set(), ya que devuelve una nueva instancia. Aquí está la heurística en la que estamos
interesados: si nada cambia, se devuelve la misma instancia. Veamos cómo podemos usar esta
propiedad de los datos de Immutable.js en nuestra vista:
import { default as React, Component } from 'react';
export default class MyView extends Component {
render() {
// Registra el hecho que hemos renderizado porque
// "shouldComponentUpdate()" prevendrá
// si la store no ha cambiado.
console.log('Rendering...');
let { state } = this.props;
return (
<p>{state.get('text')}</p>
);
}
// Desde que usamos un Immutable.js Map como
// estado de la store, sabemos que si la
// instancia es igual, nada cambia
// y no hay necesidad de renderizar.
shouldComponentUpdate(nextProps) {
return nextProps.state !== this.props.state;
}
}
La pieza clave de este componente es el método shouldComponentUpdate(), que determina
que la store ha cambiado haciendo una estricta comparación de desigualdad. En los casos en los que
este componente se está renderizando mucho pero no hay necesidad de cambiar nada, esto evitará
muchas comprobaciones del árbol DOM virtual. Ahora, veamos cómo usaremos esta vista:
import React from 'react';
import { render } from 'react-dom';
import myStore from './stores/my-store';
import MyView from './views/my-view';
import { myAction } from './actions/my-action';
// El elemento contenedor DOM para nuestro componente React.
const container = document.getElementById('app');
// El payload que es enviado a "myAction()"...
var payload = 'off';
// Renderiza el componente React usando el
// estado "myStore" ...
function renderApp(state=myStore.state) {
render(
<MyView state={myStore.state} />,
container
);
}
// Re-renderiza la aplicación cuando el store cambia...
myStore.on('change', renderApp);
// Realiza el renderizado inicial...
renderApp();
// Envia "MY_ACTION" cada 0.5 segundo. Esto
// causa que el store cambie el estado y la apliación
// re-renderice.
setInterval(() => {
myAction(payload);
}, 500);
// Tras 5 segundos, cambia el payload que es
// enviado con "MY_ACTION" así que el estado
// de la store es actualmente distinto.
setTimeout(() => {
payload = 'on';
}, 5000);
Como puedes ver, las acciones que causan que nuestro vista se vuelva a renderizar se envían
constantemente. Sin embargo, ya que la llamada set() en nuestra store está devolviendo la misma
instancia cuando nada cambia, la vista en sí misma está haciendo muy poco trabajo. Entonces, una
vez que cambiemos el valor del payload después de 5 segundos, la instancia de Map de
Immutable.js cambia, y la vista se actualiza. Esta vista se renderiza un total de dos veces-la
renderización inicial y el renderizado que tiene lugar cuando los datos de la store realmente
cambian.
Puedes haber notado que esta implementación podría haber ido en otra dirección, una donde la store
no es tan ingenua como para emitir cambios cuando nada ha cambiado. Todo es cuestión de gustos
y compensaciones. El enfoque que hemos elegido requiere que las vistas tengan un papel activo en
la optimización del flujo de trabajo de renderizado. Esto es fácil de hacer con los componentes de
React, y simplifica la lógica de nuestra store. Por otro lado, podríamos preferir mantener nuestras
vistas completamente lógicas, incluyendo la comprobación shouldComponentUpdate(. Si
este es el caso, simplemente moveríamos esta lógica de nuevo en la store, y no tendría emisión de
evento de cambio si las dos instancias Immutable.js son las mismas.
Sumario
Este capítulo te introdujo a la inmutabilidad, tanto en el sentido general del término como desde el
punto de vista de la arquitectura Flux. Comenzamos el capítulo con una discusión sobre las diversas
formas en que los datos mutables pueden romper Flux. En particular, esto rompe la joya de la
corona de cualquier arquitectura Flux – el flujo de datos unidireccional. Luego, miramos a los
diferentes tipos de flujo de datos que emergen cuando empezamos a mutar datos fuera de las stores,
ya que estas son buenas cosas que hay que buscar cuando se solucionan problemas con las
arquitecturas de Flux.
Hay varias maneras en que nuestro código puede hacer datos inmutables en nuestras stores Flux, y
exploramos muchas de ellos. Los datos inmutables tienen un costo, porque el recolector de basura
necesita ejecutarse constantemente, bloqueando la ejecución de otro código JavaScript, para recoger
todas estas copias extra de los objetos. Nos fijamos en cómo minimizar estas estas asignaciones
extra de memoria y cómo compensar el coste total del uso de datos inmutables.
Cerramos el capítulo implementando varias stores que usaban datos de estructuras
Immutable.js. Esta librería nos facilita inmutabilidad, funcionalidad añadida y uso eficiente de
las asignaciones de memoria intermedia por defecto. En el próximo capítulo, implementaremos
nuestro propio componente dispatcher.
10
Implementando un dispatcher
Hasta este punto en el libro, hemos confiado en la implementación de referencia del dispatcher
Flux. Esto no tiene nada de malo, es un software funcional y el dispatcher no tiene muchas piezas
móviles. Por otro lado, se trata de sólo una implementación de referencia de una idea más amplia -
que las acciones deben ser enviadas a las stores y las dependencias de store deben gestionarse.
Empezaremos hablando de la interfaz abstracta del dispatcher que es requerida por las arquitecturas
de Flux. A continuación, discutiremos algunas de las motivaciones detrás de la implementación de
nuestro propio dispatcher. Finalmente, dedicaremos el resto del capítulo a implementar nuestro
propio módulo de dispatcher, y después mejorando nuestros componentes de store de modo que
sean capaz de interactuar sin problemas con el nuevo dispatcher.

Interfaz abstracta del dispatcher


La idea con cualquier implementación de referencia es ilustrar directamente, usando código, cómo
se supone que algo debe funcionar. La implementación de referencia en Facebook de Flux hace
justamente eso: podemos usarlo en una arquitectura Flux real y obtener resultados. También
ganamos una comprensión de la interfaz abstracta del dispatcher. Dicho de otra manera, la
implementación de referencia es un tipo de requisitos de software, expresados en forma de código.
En esta sección, trataremos de entender mejor cuáles son estos requisitos mínimos antes de
zambullirnos en nuestra propia implementación de dispatcher. La primera pieza esencial de
funcionalidad que el dispatcher debe implementar es el registro de store para que el dispatcher
pueda enviar payloads. Entonces, necesitamos el envío real que itera sobre las stores registradas y
entrega payloads. Finalmente, tenemos que pensar en la semántica de la dependencia mientras
enviamos payloads.

Registro de store
Cuando instanciamos una store, tenemos que decírselo al dispatcher. De lo contrario, el dispatcher
no sabe de la existencia de la store. El patrón generalmente se parece a esto:

El dispatcher mantiene una colección interna de callbacks para ser ejecutadas cuando se envía una
acción. Simplemente necesita iterar sobre esta colección de callbacks llamando a cada uno de ellas
por turno. Esto es tan fácil como suena, cuando todo durante una ronda de actualización de Flux
está sincronizado. La pregunta es, ¿por qué querríamos cambiar la forma en que funciona el proceso
de registro de la store?
¿Tal vez en lugar de registrar una función callback dentro del constructor de la store, queremos
pasar al dispatcher una referencia a la propia instancia de la store? Entonces, cuando llegue el
momento para notificar a la store sobre una acción que se ha enviado, el dispatcher iteraría sobre
una colección de instancias de store y llamaría a algún método predefinido. La ventaja de este
enfoque sería que, puesto que el disptacher tiene una referencia al store, podría acceder a otros
metadatos sobre la store, como sus dependencias.
Exploraremos esta idea más a fondo una vez que comencemos a escribir código, un poco más
adelante en el capítulo. La conclusión es la siguiente: necesitamos un medio para decirle al
dispatcher que a una determinada instancia de la store le gustaría recibir notificaciones de acción.

Envío de payloads
El envío real de payloads es bastante simple. La única parte complicada es el manejo de las
dependencias entre stores; hablaremos de eso a continuación. Por ahora, imagínese una arquitectura
en la que no haya dependencias entre stores. Es sólo una simple colección para iterar, llamando a
cada función con el payload de acción como argumento. He aquí una ilustración del proceso:

Aparte de la gestión de la dependencia, ¿hay algo más que le falte a esta representación? Bueno,
hay una situación en la que podríamos encontrarnos con envíos anidados. Éstos se prohíben
terminantemente en arquitecturas Flux pues interrumpirían rondas de actualización sincronas
unidireccionales. De hecho, la implementación de referencia de el dispatcher de Facebook rastrea el
estado de cualquier ronda de actualización y captará esto si ocurre.
Esto no significa que un componente del dispatcher que implementemos tenga que comprobar tales
condiciones. Sin embargo, nunca es una mala idea fallar rápido con algo tan perjudicial para la
naturaleza de la arquitectura.
Otra cosa que vale la pena pensar es la necesidad de llamar a cada store registrada en una ronda de
actualización determinada. Claro, tiene sentido en cuanto a la consistencia - tratar cada store igual y
notificarles sobre todas las cosas. Por otro lado, podríamos tener una gran aplicación con cientos de
acciones enviadas. ¿Tendría sentido enviar siempre acciones a las stores que nunca responden a
ellas? Cuando implementamos nuestro propio componente del dispatcher, somos libres de pensar en
cómo podemos implementar tales heurísticas que benefician a nuestra aplicación a la vez que se
mantiene fiel a los principios de Flux.

Gestión de dependencias
Quizás el aspecto más difícil de las acciones enviadas es asegurarse de que las relaciones de store se
gestionan correctamente. Por otra parte, el dispatcher sólo tiene que asegurarse de que se llama a los
manejadores de las acciones de la stores en el orden correcto. Aquí se ilustran las acciones enviadas
con dependencias:

Mientras las stores que caen en el lado derecho de las llamadas waitFor() reciban el envío de
notificaciones primero, estará todo bien. Así que, en esencia, las relaciones de store son un
problema de ordenación en lo que respecta al dispatcher. Ordena las callbacks de tal manera que
satisface el gráfico de dependencias, luego itera y llama a cada handler.
Aquí está la cosa - ¿realmente queremos confiar en el método del dispatcher waitFor() como un
medio para gestionar relaciones de tienda? Posiblemente una mejor manera de manejar esto sería
declarar una serie de stores de las que dependemos. A continuación, se pasaría al dispatcher en el
momento del registro, y ya no necesitaríamos las llamadas waitFor().
Tenemos el plan básico de lo que se requiere para implementar nuestro propio dispatcher. Pero antes
de seguir adelante con la implementación, pasemos un poco más de tiempo discutiendo los retos a
los que se enfrenta el dispatcher de Facebook.

Desafíos con el despachador


En la sección anterior, hemos vislumbrado algunos de los desafíos potenciales con la
implementación de referencia del dispatcher de Facebook en Flux. En esta sección, desarrollaremos
algunos de estos razonamientos, en un intento de proporcionar motivación para implementar
nuestro propio dispatcher personalizado.
En esta sección, reiteraremos el hecho de que el paquete Flux NPM existe principalmente como
herramienta educativa. Dependiendo de un paquete como este está bien, especialmente porque hace
el trabajo, pero repasaremos algunos de los riesgos que algo como esto conlleva en un contexto de
producción. Entonces, hablaremos sobre el hecho de que los componentes del dispatcher son casos
aislados y probablemente no necesiten serlo.
Luego pensaremos en el proceso de registro de la store y en el hecho de que es un proceso más
manual de lo que tiene que ser. Finalmente, volveremos a tocar el problema de la gestión de
dependencias de la store con una discusión sobre waitFor() y posibles alternativas declarativas.

Propósitos educativos
El paquete Flux NPM de Facebook, como sabemos, proporciona una implementación de referencia
de un dispatcher. La mejor manera de aprender cómo se supone que funciona un componente de
este tipo es escribir el código que lo usa. Es para propósitos educativos, en otras palabras. Esto nos
eleva del suelo rápidamente, mientras descubrimos la mejor manera de escribir código Flux.
Facebook podría haber omitido fácilmente la implementación del dispatcher y dejado en manos de
los programadores que leen la documentación de Flux averiguarlo. El código es altamente
educativo, y sirve como una forma de documentación. Incluso si decidimos que no estamos locos
por cómo se implementa el dispatcher, al menos podemos leerlo para averiguar qué debe hacer el
dispatcher.
Entonces, ¿existe algún riesgo si utilizamos este paquete en un entorno de producción? Si usamos el
dispatcher Flux por defecto en nuestro proyecto, y todo lo que hemos desarrollado contra él
funciona, no hay razón por la que no podamos usarlo en una aplicación de producción. Si funciona,
funciona. Sin embargo, el hecho de que se trate de una implementación de referencia destinada a
propósitos educativos probablemente significa que no hay un desarrollo serio en él. Tome React
como ejemplo, donde millones de personas usan este software en un entorno de producción. Hay
motivación para que esta tecnología avance y se mejore a sí mismo. Esto simplemente no es el caso
con la implementación de un dispatcher de referencia. Crear el nuestro definitivamente vale la pena
pensar en ello, especialmente si hay posibilidad para mejorar.

Dispatchers Singleton
Si usamos el dispatcher de Flux desde Facebook, tenemos que instanciarlo, ya que es sólo una clase.
Sin embargo, como sólo hay una ronda de actualización en un momento dado, no hay necesidad de
más de una instancia de dispatcher en toda la aplicación. Este es el patrón singleton, y no siempre es
el mejor patrón a usar. Para empezar, es innecesario.
Por ejemplo, en cualquier momento que queramos enviar una acción, necesitamos acceder al
comando dispatch() del dispatcher. Esto significa que tenemos que importarlo e invocar el
método usando la instancia como contexto, como por ejemplo: dispatcher.dispatch() . Lo
mismo ocurre con el método register(); cuando una store quiere registrarse con el dispatcher,
primero necesita tener acceso a la instancia antes de llamar al método.
Por lo tanto, parece que esta instancia del despachador singleton no tiene otro propósito real que el
de interponerse en el camino y hacer que el código sea más verboso. ¿Y si en lugar de una instancia
de clase singleton, el dispatcher fuera un simple módulo que exportara las funciones relevantes?
Esto simplificaría enormemente el código en lugares donde se requiere el dispatcher, que
probablemente son bastantes si nuestra aplicación tiene muchas stores y acciones.

Registro manual de la store


Una invariante de las arquitecturas de Flux es que las stores están conectadas a los dispatchers. No
hay otra forma de cambiar el estado de una store, que no sea enviando una acción. Así que a menos
que queramos una store estático que nunca cambie de estado, necesitamos registrarla en el
dispatcher. Todas las stores de ejemplo que hemos visto hasta ahora han puesto a sus dispatchers en
el constructor. Aquí es donde se manejan las acciones que podrían potencialmente cambiar el estado
de una store.
Dado que el registro en el dispatcher es un hecho, ¿realmente necesitamos registrarnos
explícitamente una función callback cuando se crea cada tienda? Un enfoque alternativo podría
involucrar una clase de tienda base que se encarga de este registro por nosotros; esto no es
necesariamente un problema específico del dispatcher.
El otro aspecto del registro de la store que se siente innecesario en su mayor parte es la gestión de
los identificadores de los dispatchers. Por ejemplo, si implementamos una store que depende de otra
store, tenemos que hacer referencia al ID de envío de esa otra store. El motivo de los IDs es simple:
una función callback no identifica la store. Así que tenemos que utilizar el dispatcher para asignar el
ID de callback a la store. Todo el enfoque sólo se siente desordenado, así que cuando
implementamos nuestro propio dispatcher, podemos eliminar estas identificaciones de envío por
completo.

Gestión de dependencias propensas a errores


La última queja que queremos tratar con el dispatcher de Flux de Facebook por defecto es la forma
en que se manejan las dependencias entre stores. El mecanismo waitFor() hace su trabajo
bloqueando la ejecución del handler hasta que todas sus dependencias hayan manejado la acción.
De esta manera, sabemos que la store de la que dependemos está siempre al día. El problema es que
waitFor() es un poco propenso a errores.
Por un lado, siempre tiene que estar en el mismo lugar, justo en la parte superior del manejador de
acción de la store. Tenemos que recordar usar los IDs de envío de las stores de las que dependemos
para que waitFor() sepa qué tiendas procesar a continuación. Un enfoque más declarativo
significaría que podríamos establecer las dependencias de la store como un conjunto de referencias
de store o algo parecido. De este modo, se declaran las dependencias fuera de la función callback y
son un poco más obvias. Descubriremos una forma de implementar esto en nuestro dispatcher, que
ahora comenzaremos.

Construcción de un módulo de dispatcher


En esta sección, vamos a implementar nuestro propio módulo de dispatcher. Esto servirá como
reemplazo para la implementación de referencia de Facebook en la que hemos confiado hasta ahora
en este libro. Primero, pensaremos en cómo el dispatcher rastreará las referencias a los módulos de
la store. Luego, discutiremos las funciones que este módulo necesita exponer, seguidas de
un recorrido por la implementación de dispatch(). Por último, averiguaremos cómo deseamos
gestionar la gestión de dependencias con este módulo de dispatcher.

Encapsulación de referencias de store


El primer aspecto de nuestro módulo de dispatcher a considerar son las stores mismas. Con la
implementación de referencia de Facebook, no hay referencias a las stores-sólo referencias a las
funciones callback. Es decir, cuando nos registramos en el dispatcher de Facebook estamos pasando
al método register() una función en lugar de la instancia de la propia store. Nuestro módulo de
dispatcher se aferrará a las referencias de las stores en lugar de sólo a las funciones callbacks. He
aquí un diagrama que ilustra el enfoque adoptado por la implementación de referencia:
Cada vez que se llama a register(), añade una función callback a la lista de callbacks que debe
procesar el dispatcher cada vez que se envía una acción. Sin embargo, la desventaja es que el
dispatcher podría necesitar acceso a la store para obtener funcionalidades más avanzadas que
queremos implementar, como veremos en breve. Así que vamos a querer registrar la instancia de la
store en sí, en lugar de sólo una función callback. Este enfoque se ilustra aquí:

La lista de funciones callback es ahora una lista de instancias de store y, cuando se envía una
acción, el dispatcher tiene acceso a los datos de store, lo que es útil para cosas como métodos y
listas de dependencias. La desventaja aquí es que las funciones callback son más genéricas, y
simplemente son llamadas por el dispatcher. Como veremos momentáneamente, hay ventajas en
este enfoque que simplifican el código de store.

Gestión de dependencias
Lo primero que pensaremos en términos de la implementación de nuestros dispatchers es cómo se
gestionan las dependencias entre stores. El enfoque estándar es implementar un método
waitFor() que bloquea la ejecución en la función manejadora de la store hasta que las stores de
los que depende han sido manipulados. Como sabes ahora, este enfoque puede ser problemático
debido al hecho de que se utiliza dentro de la función manejadora. Un más enfoque declarativo es lo
que estamos buscando con nuestra implementación.
La idea es que la lista de stores de las que dependen se declare como propiedad de la store. Esto
permite que se consulte la store de las que otras stores dependen. También se elimina el aspecto de
gestión de dependencias de las stores del código del manejador que es se supone que se centra en
las acciones. He aquí una comparación visual de los dos enfoques:
Intentar acceder a dependencias que están especificadas en waitFor() es como pelar una cebolla,
están por capas. Nuestro objetivo es separar el código del handler de la especificación de la
dependencia. Entonces, ¿cómo lo hacemos exactamente?
En lugar de tratar de manejar dependencias durante el proceso de envío, podríamos clasificar
nuestras dependencias a medida que se registran las stores. Si una tienda tiene sus listado de
relaciones en una propiedad, el despatcher puede organizar la lista de stores de tal manera que
satisfaga esas dependencias. Aquí hay una implementación de una función de register() para
nuestro módulo de dispatcher:
// Esto es usado para stores así ellas pueden registrarse
// a si mismas en el dispatcher.
export function register(store) {
// No permitamos que se registre a si misma dos veces...
if (stores.includes(store)) {
throw `Store ${store} already registered`;
}
// Añade una referencia "store" a "stores".
stores.push(store);
// Ordena nuestras stores basado en sus dependencias. Si una store
// depende de otra store, la otra store es
// considera “menor qué” la store. Esto significa que
// las dependencias serán satisfechas si las stores son
// procesadas ordenadas.
stores.sort((a, b) => {
if (a.deps.includes(b)) {
return 1;
}
if (b.deps.includes(a)) {
return -1;
}
return 0;
});
}
Esta es la función que las stores pueden utilizar para registrarse. Lo primero que esta es comprobar
si la tienda ya ha sido registrada en el dispatcher. Esta es una comprobación fácil de realizar, ya que
las referencias se almacenan en una matriz; nosotros podemos usar el método includes(). Si la
store aún no está registrada, entonces podemos poner la tienda en la matriz.
A continuación, nos encargamos de las dependencias de las stores. Cada vez que una store se
registra, volvemos a re-ordenar la matriz stores. Este orden se basa en la propiedad deps de la
store. Aquí es donde el se declaran las dependencias de la store. El comparador de clasificación es
sencillo. Es según si la Store A depende de la Store B o viceversa. Por ejemplo, digamos que estas
stores se registraron en el siguiente orden:

Ahora, asumamos que las siguientes dependencias de store han sido declaradas:

Esto significa que la Store A depende de Store B y Store D. Tras el registro de cada una de esas
stores, el orden de la lista de stores en nuestro módulo dispatcher sería como sigue:

Ahora la lista de stores está en un orden que satisface las relaciones de las stores. Cuando el
dispatcher itera sobre la lista de stores y llama a cada manejador de store, se hará en el orden
correcto. Puesto que la Store A depende de las Store C y Store D, lo único que importa es que estas
dos stores se manejen primero. El orden de la Store A y la Store C son intrascendentes ya que no se
declara ninguna dependencia entre ellas. Ahora, veamos cómo implementar la lógica de envío de
nuestro módulo.

Envío de acciones
En la implementación de referencia de Facebook de un dispatcher Flux, el mecanismo de envío es
un método de una instancia del disptacher. Ya que realmente no hay necesidad de una instancia de
dispatcher singleton, nuestro dispatcher es un simple módulo con un par de funciones expuestas,
incluyendo una función dispatch(). Gracias a la lógica de clasificación de dependencias, hemos
implementado en la función register(); el flujo de trabajo de dispatch() será agradable y
sencillo. Echemos un vistazo a este código ahora:
// Usado por las funciones creadoras de acciones para envia un
// payload de acción.
export function dispatch(payload) {
// El dispatcher está ocupado, significa que hemos
// llamado a “dispatch()” mientra una ronda de actualización
// estaba siendo realizada.
if (busy) {
throw 'Nested dispatch() call detected';
}
// Marca al dispatcher como "busy" antes de que
// comencemos a llamar a cualquier manejador de store.
busy = true;
// La acción "type" determina el método
// que vamos a llamar en la store.
let { type } = payload;
// Itera sobre cada store registrada, buscando
// por un nombre de método que concuerde con "type". Si lo encuentra,
// lo llamamos, pasandole el "payload" que
// será enviado.
for (let store of stores) {
if (typeof store[type] === 'function') {
store[type](payload);
}
}
// El dispatcher no estará ocupado más, así que lo desmarcamos.
busy = false;
}
Puedes ver que hay una variable busy que está seleccionada en la parte superior de la función. Esto
se establece justo antes de que empecemos a llamar a los envíos de las stores. Esencialmente, esto
verifica cualquier cosa que llame a dispatch() como resultado de que una store maneje una
acción. Por ejemplo, podríamos llamar accidentalmente a dispatch() desde una store o desde
una vista que esté escuchando a una store. Esto no está permitido ya que rompe el flujo de datos
unidireccional de nuestra arquitectura Flux. Cuando esto sucede, es mejor detectarlo y fallar rápido
que dejarlo que las rondas de actualización anidadas sigan su curso.
Aparte de la lógica de manejo de estado ocupado, esta función itera sobre las stores y verifica si hay
un método apropiado para llamar. El nombre del método se basa en el tipo de acción. Por ejemplo,
si la acción es MY_ACTION y la store tiene un método del mismo nombre, entonces ese método es
invocado con el payload como argumento de método. El proceso se visualiza aquí:

Esta es la diferencia con el enfoque estándar de la declaración switch que hemos estado usando
en este libro hasta ahora. En lugar de eso, es el dispatcher quien debe localizar el código apropiado
para que se ejecute dentro de la store. Esto significa que si la store no implementa un método que
corresponde a la acción que se ha enviado, es ignorada por la store. Esto es algo que sucede a
menudo dentro de nuestros manejadores de envío de la store, sólo que ahora sucede más
eficientemente porque evita la revisión por parte de un switch. En la próxima sección, veremos
cómo nuestras stores pueden trabajar con esta nueva implementación de dispatcher. Pero primero,
aquí está el módulo de dispatcher en su totalidad:
// Referencia a las stores registradas...
const stores = [];
// Es true cuando el dispatcher está realizando
// una ronda de actualización. Por defecto, no está ocupado.
var busy = false;
// Es usado por los stores así pueden registrarse
// a sí mismos con el dispatcher.
export function register(store) {
// No permite a una store registrarse a sí misma dos veces...
if (stores.includes(store)) {
throw `Store ${store} already registered`;
}
// Añade la referencia a "store" en "stores".
stores.push(store);
// Ordena nuestras stores basado en dependencias. Si una store
// depende de otra store, la otra store es
// considerada “menor que” la store. Esto significa que
// las dependencias serán satisfechas is las stores son
// procesadas en orden.
stores.sort((a, b) => {
if (a.deps.includes(b)) {
return 1;
}
if (b.deps.includes(a)) {
return -1;
}
return 0;
});
}
// Usado por las funciones creadoras de acciones para enviar un
// payload de acción.
export function dispatch(payload) {
// El dispatcher está ocupado, significa que hemos
// llamado a “dispatch()” mientras una ronda de actualización
// estaba tomando lugar.
If (busy) {
throw 'Nested dispatch() call detected';
}
// Marca al dispatcher como "busy" antes de que
// comencemos a llamar a cualquier manejador.
busy = true;
// El "type" de acción determina el método
// que vamos a llamar en la store.
let { type } = payload;
// Itera sobre cada store registrada, buscando
// por un método que coincida con "type". Si lo encuentra,
// entonces lo llamamos, pasandole el "payload" que
// fue enviado.
for (let store of stores) {
if (typeof store[type] === 'function') {
store[type](payload);
}
}
// El dispatcher no está ocupado, así que lo desmarcamos.
busy = false;
}

Mejorar el registro de stores


No podemos mejorar el flujo de trabajo del dispatcher sin mejorar el flujo de trabajo de nuestras
stores. Afortunadamente, el trabajo duro ya ha sido implementado por el dispatcher. Sólo
necesitamos implementar nuestras stores de una manera que utilice mejor las mejoras que hemos
hecho al dispatcher. En esta sección, discutiremos la implementación de una clase de store base,
seguida de algunos ejemplos de implementaciones de stores que la extienden e implementan sus
propios métodos de acción.

Clase base de store


El nuevo dispatcher que acabamos de implementar tiene algunas diferencias importantes con la
implementación de referencia de Facebook. Las dos diferencias clave son que la store registra una
instancia de sí misma en lugar de un callback, y que la store necesita implementar métodos de
acción. La clase base de store debería ser capaz de registrarse con el dispatcher cuando se crea. Esto
significaría que las stores que extienden de esta clase base no tendrían que preocuparse en absoluto
por el dispatcher, implementando métodos de acción que cambien el estado de la store en
consecuencia.
La capa del dispatcher, la store base, y las stores que extienden de ella se ilustra aquí:

Vayamos adelante y miremos la implementación de una clase base de store ahora. Entonces,
implementaremos algunas stores que extiendan de ella así podremos ver nuestro nuevo módulo
dispatcher en acción:
import { EventEmitter } from 'events';
import { register } from './dispatcher';
// Exportamos la store base de otras de las que extienda.
export default class Store extends EventEmitter {
// El constructor asigna el “state” inicial de la
// store, así como cualquier dependencia "deps" con
// otras stores.
constructor(state = {}, deps = []) {
super();
// El estado de las stores y sus dependencias. La propiedad
// “deps” es actualmente requerida por el
// dispatcher.
this.state = state;
this.deps = deps;
// Registra la store en el dispatcher.
register(this);
}
// Este es un sencillo método auxiliar que cambia el
// estado de la store, asignado la propiedad
// "state" y emitiendo el evento "change”.
change(state) {
this.state = state;
this.emit('change', state);
}
}
Eso es todo, bastante simple, ¿verdad? El constructor acepta el estado inicial de la store y una serie
de dependencias de store. Ambos argumentos son opcionales, ya que tienen valores por defecto en
los argumentos. Esto es especialmente importante para la propiedad deps porque nuestro módulo
de dispatcher espera que esté. Luego, llamamos a la función register() para que el dispatcher
sea automáticamente consciente de cualquier store. Recuerde, una store Flux es de ninguna utilidad
si es incapaz de manejar las acciones mientras se envían.
También hemos añadido un pequeño y práctico método change() que actualiza el estado y emite
el evento de cambio por nosotros. Ahora que tenemos una clase base de store, somos libres de
implementar pequeños métodos auxiliares como este para reducir el código duplicado de la store.

Un método de acción
Completemos nuestro ejemplo que hemos estado realizando durante algunas secciones. Para ello,
vamos a implementar algunas stores que amplían la store base que acabamos de crear. Aquí está la
primera store:
import Store from '../store';
import second from './second';
import third from './third';
// El estado inicial de la store, se lo
// pasaremos a "super()" en el constructor.
const initialState = {
foo: false
};
// Las dependencias que esta store tiene sobre otras
// stores. En este caso, es "second" y
// "third". Estas, también, son pasada a través de
// "super()".
const deps = [ second, third ];
class First extends Store {
// La llamada a "super()" tiene asignará
// el estado inicial de la store, y sus dependencias
// por nosotros.
constructor() {
super(initialState, deps);
}
// Llamada en respuesta de la acción "FOO"
// enviada…
FOO(payload) {
this.change({ foo: true });
}
// Llamada en respuesta de la acción "BAR"
// enviadad...
BAR(payload) {
this.change(Object.assign({
bar: true
}, this.state));
}
}
export default new First();
Esta store tiene todas las partes móviles relevantes para trabajar con nuestra nueva clase base de
store y nuestro nuevo módulo dispatcher. Puedes ver en el constructor que estamos pasando los
valores initialState y deps al constructor de Store. También puedes ver que tenemos dos
métodos de acción implementados en esta store: FOO() y BAR() . Esto significa que si se envían
acciones con un tipo de FOO o BAR, esta store responderá a ellas. Ahora vamos a implementar las
dos stores de las que depende esta store:
Si no soportas en absoluto el aspecto de los nombres de métodos en mayúsculas, no
dudes en cambiar el caso de los tipos de acción que se envían. Otra alternativa es
implementarlo sin distinción entre mayúsculas y minúsculas en el dispatcher. La
desventaja de esta última opción es que perderíamos el mapeo directo del tipo de
acción al nombre del método. Ten cuidado con lo que deseas.
import Store from '../store';
import third from './third';
class Second extends Store {
// La llamada a "super()" asigna el estado
// inicial por nosotros.
constructor() {
super({
foo: false
});
}
// Llamada en respuesta a la acción "FOO"
// enviada...
FOO(payload) {
this.change({ foo: true });
}
// Llamada en respuesta de la acción "BAR"
// enviada. Advierte que dependemos
// de la store "third" store, aún
// no hemos hecho esta dependencia explícita.
// Esto podría convertirse en un problema.
BAR(payload) {
this.change({
foo: third.state.foo
});
}
}
export default new Second();
La store Second es similar a la store First. Extiende la clase base Store y establece un estado
predeterminado. También responde a dos acciones, como podemos ver por el nombre de los dos
métodos. Sin embargo, esta store no declara ninguna dependencia, pero depende claramente de la
tercera store en el manejador de acción BAR(). Esto puede o no funcionar, dependiendo en el lugar
en que la store third aterrice en la recogida de los stores que obren en poder del dispatcher. Si
declaramos third como una dependencia, entonces sabemos con certeza que siempre será
actualizado antes de esta store. Echemos un vistazo a nuestra última tienda:
import Store from '../store';
class Third extends Store {
// La llamada a "super()" asigna el estado
// inicial por nosotros...
constructor() {
super({
foo: false
});
}
// Llamada en respuesta de la acción “FOO”
// enviada.
FOO(payload) {
this.change({ foo: 'updated' });
}
}
export default new Third();
Una vez más, esta store sigue los mismos patrones que sus dos sucesoras. La diferencia clave es que
no tiene ningún manejador de acción BAR(). Esto significa que no se llamará a nada en esta store
cuando se envíen acciones de BAR. Esto contrasta con nuestros handlers anteriores, donde cada
acción se habría canalizado a través de una sentencia switch sólo para ser ignorada. Finalmente,
echemos un vistazo a main.js para unir todo esto:
import first from './stores/first';
import second from './stores/second';
import third from './stores/third';
import { foo } from './actions/foo';
import { bar } from './actions/bar';
// Registra el estado de cada store así como sus cambios...
first.on('change', (state) => {
console.log('first', state);
});
second.on('change', (state) => {
console.log('second', state);
});
third.on('change', (state) => {
console.log('third', state);
});
foo();
// →
// third {foo: "updated"}
// second {foo: true}
// first {foo: true}
bar();
// →
// second {foo: "updated"}
// first {bar: true, foo: true}
Advierte que la salida foo() refleja el orden de dependencia correcta y que la salida bar()
refleja la inexistencia del manejador de acción en Third.

Sumario
En este capítulo, aprendiste acerca de algunas de las limitaciones inherentes al componente
dispatcher Flux de Facebook. Para empezar, no es apto para entornos de producción, porque es una
implementación de referencia para los patrones Flux. Somos libres de implementar estos patrones
de dispatcher como queramos.
Los aspectos esenciales de un dispatcher son la capacidad de registrar código de store que maneja
las acciones a medida que se envían y la capacidad de realizar los envíos. Dada la simplicidad de
los requisitos, no tiene sentido implementar otra clase de Singleton. En su lugar, el dispatcher sólo
necesita exponer un register() y dispatch().
El gran cambio con nuestra implementación fue con respecto a la gestión de dependencias. En lugar
de calcular dependencias cada vez que se envía una acción, la función register() ordena la
colección de stores de tal manera que satisface las necesidades de relaciones entre stores. Luego
implementamos una clase de store base que se utiliza para simplificar nuestro código de store
registrando automáticamente la store en el dispatcher por nosotros.
En el siguiente capítulo, veremos los componentes vista que dependen de tecnologías distintas de
ReactJS para renderizarse.
11
Componentes Vista Alternativos
La documentación de Flux no tiene mucho que decir sobre los componentes de la vista. Y sin
embargo, las vistas son una parte esencial de cualquier arquitectura Flux. Tal vez lo que los autores
de Flux realmente quieren decir es que Flux no se preocupa por los mecanismos que solían expresar
nuestras vistas, siempre y cuando se manifiesten de alguna manera.
No es ningún secreto que Flux fue diseñado pensando en React. Facebook ya tenía React para sus
componentes de vista - Flux fue la pieza que faltaba y que permite formular una arquitectura front-
end completa. Comenzaremos este capítulo con una discusión sobre lo que hace que React se adapte
tan bien a las arquitecturas Flux. Entonces sopesaremos estos beneficios frente a los inconvenientes
de React.
A continuación, pasaremos algún tiempo construyendo vistas usando jQuery y el motor de plantillas
Handlebars. Estas son dos tecnologías populares que probablemente se han cruzado en el camino de
cualquier desarrollador en algún momento. Luego cerraremos el capítulo pensando en vistas que no
requieren una tecnología de renderizado específica, lo que permite que seamos ágiles con nuestras
vistas y adoptemos las más novedosas cuando lleguen.

ReactJS se adapta bien a Flux


No es de extrañar que React se adapte bien a las arquitecturas Flux. Ambas tecnologías fueron
creadas por la misma compañía, y ambos resuelven problemas complementarios. En esta sección,
nos adentraremos en algunos de los detalles de qué es lo que hace que React funcione tan bien con
Flux. Comenzaremos mirando los flujos unidireccionales encontrados tanto en Flux como en React.
A continuación, discutiremos la idea de que volver a renderizar estructuras DOM es más fácil que
manipular nodos DOM específicos, y por qué es un buen ajuste para la store modificar
controladores de eventos. Finalmente, hablaremos sobre la relativamente pequeña huella de código
de los componentes React.

ReactJS es unidireccional
El flujo de datos en una arquitectura Flux es unidireccional. Empieza con una acción y termina con
las actualizaciones de las vistas: no hay otra forma de que los datos entren en un componente vista.
React comparte esta misma filosofía unidireccional con Flux. Los datos fluyen a un componente
raíz de React y se filtran a cualquier componente usado en la composición desde la raíz. Este
proceso es recursivo en la jerarquía de componentes.
Los datos fluyen hacia las stores Flux a través de acciones, y fluyen hacia fuera como eventos de
cambio. React mantiene este flujo unidireccional. Una vez que el componente React tiene que
volver a renderizarse a sí mismo basado en el estado de una store, el flujo se hace. La única opción
es empezar de nuevo enviando una nueva acción. El flujo entre Flux y los componentes React se
ilustra aquí:
Los primeros tres elementos en nuestro flujo de datos son entidades Flux. Cualquier flujo de datos
dado es pateado cuando se envía una acción. A continuación, la propia acción entra en el dispatcher
y es enviado a todas las stores. A continuación, la store realiza los cambios de estado que considere
oportunos. Desde aquí, el flujo de datos se transfiere al componente React. Aquí es donde hemos
especificado la estructura de lenguaje de marcas que queremos renderizar, usando JSX. A
continuación, el componente consulta con el DOM virtual para averiguar qué cambios, si los
hubiere, se deberían realizar en el DOM. Una vez realizados estos cambios, se ha llegado al final
del flujo de datos.
El flujo que hemos descrito aquí para los componentes de React no se vería diferente incluso si no
fueran parte de una arquitectura Flux. Los componentes de Flux sólo añaden cambios de estado
predecibles de forma síncrona, antes de entregar los datos a componentes para el renderizado. Sin
Flux, React aún tendría que empezar desde arriba y pasar los datos hacia abajo para que el proceso
de re-renderizado pueda comenzar. Esto encaja muy bien con los eventos de modificación emitidos
por las tiendas Flux.
Lo que no encaja tan bien con React es la idea de un enlace bidireccional de datos. A algunas
personas le encanta la idea y se han encontrado formas de hacer que funcione con React, pero no
divaguemos. Para que la unión bidireccional sea efectiva, nuestros componentes vista deben estar
muy cerca a datos mutables. Entonces, la vista puede escuchar directamente estos datos para volver
a renderizarlos por si misma. No estamos preparados para manejar esto con arquitecturas Flux, por
no hablar de React. La idea que podemos mutar directamente el estado de algo sin entrar primero en
un flujo de trabajo que gestiona la actualización sincrona del estado de toda la aplicación, va en
contra de la idea de Flux. En pocas palabras, las arquitecturas Flux favorecen los flujos de datos
unidireccionales con resultados predecibles y React ayuda en esta misión.

Renderizar nuevos datos es fácil


Una cosa que realmente destaca de ReactJS es su capacidad de volver a renderizar todo el árbol
DOM. Bueno, cualquier código JavaScript puede reemplazar un árbol DOM existente
construyéndolo de nuevo. React utiliza lo que se llama un DOM virtual para comparar los
elementos existentes que el usuario está visualizando contra los nuevos elementos que acabamos de
presentar. En lugar de reemplazar el árbol al completo, React sólo tocara los lugares en los que los
dos árboles difieran. Aparte de la heurística que React ha construido para sí, el rendimiento
fundamental proviene del hecho de que el DOM virtual está en la memoria Javascript-no tenemos
que buscar elementos en el árbol DOM real. Consultar al DOM puede tener consecuencias
negativas para el rendimiento.
Para evitar estos problemas de rendimiento, nuestro código de vista puede generar consultas
específicas que son eficientes de ejecutar, y sólo obtenemos los elementos exactos que necesitamos.
Nuestro código de vista puede también almacenar en caché los elementos específicos que necesita.
El problema con este enfoque es que se siente fragmentado una vez que tenemos más de unos pocos
componentes de vista. Es difícil para los componentes compartir código cuando todos están
diseñados para su propio rendimiento específico y esto depende en gran medida de la estructura
DOM del componente.
Es más natural para los programadores poder decir aquí hay una instantánea de cómo estas vistas
deben mostrarse en un momento dado. No deberíamos tener que separar el DOM y decir que este
div debería verse así mientras que este span debería estar oculto y así sucesivamente. Esta es la
razón por la que funciona JSX; podemos visualizar más fácilmente lo que la salida de nuestro
componente va a mostrar, porque está estructurado como los elementos están estructurados.

Pequeña cantidad de código


Los componentes React generalmente tienen menos código que los componentes vista que tienen
gran cantidad de indispensable código DOM manipulado. React no tiene este tipo de código porque
sólo necesita expresar la estructura del DOM a través de JSX. Sin embargo, sin Flux como
arquitectura, una aplicación que utilice React probablemente encontrará que el archivo React
contienen mucho más código de transformación de datos.
Por ejemplo, cuando los componentes de React se montan en el DOM, es posible que necesitemos
realizar algún tipo de transformación en los datos que provienen de alguna fuente, tal vez una
respuesta AJAX. Con Flux, la fuente es siempre el estado del store, por lo que sabemos que las
transformaciones de datos ya han ocurrido en el momento en que se entregan a las vistas de React.
Recuerde, son las vistas las que guían la estructura de nuestro estado de store, no las stores las que
impulsan cómo deben estructurarse nuestras vistas.
El código de gestión de eventos es otra área en la que los componentes de React pueden tener una
pequeña cantidad de código. Bueno, esto tiene dos dimensiones. Primero, los manejadores de
eventos en React se declaran directamente en el JSX, por lo que son parte de la estructura del árbol
DOM como cualquier otra propiedad del elemento - no hay necesidad de insertar los elementos en
el DOM y luego buscarlos de nuevo más tarde para que podamos adjuntar una función manejadora
de eventos para ellos. La segunda dimensión no es específica de React, sino más bien de un
fenómeno de Flux. Los propios manejadores de eventos suelen ser sólo funciones creadoras de
acciones. Toda la lógica que habría estado en nuestras vistas es ahora parte de nuestras stores.

Las desventajas de ReactJS


Ahora que tiene una apreciación de los beneficios de usar ReactJS como vista en una arquitectura
Flux, es hora de ver algunas de las desventajas. Todo tiene desventajas: no existe tal cosa como una
tecnología perfecta. Así que estas cosas son dignas de consideración en el contexto de una
arquitectura Flux para su aplicación.
Primero, consideraremos el consumo de memoria. React es una biblioteca bastante grande y tiene
un notable impacto en el tiempo de carga de la aplicación. Sin embargo, esto es de menor
importancia comparado con la cantidad de memoria consumida por el DOM virtual. Lo siguiente
que haremos será considerar la sintaxis JSX en nuestros módulos JavaScript y los problemas que
para aquellos que no están acostumbrados a mezclar otros lenguajes en sus módulos JavaScript.

Virtual DOM y memoria


Las aplicaciones JavaScript deben esforzarse por ahorrar la mayor cantidad de memoria posible.
Las aplicaciones que utilizan una gran cantidad de memoria son inherentemente más lentas que las
que usan menos memoria porque necesitan realizar más trabajo. Por ejemplo, si necesitamos buscar
algo en una colección, obviamente se necesitarán más recursos de computación si la colección tiene
una tonelada de objetos en ella, a diferencia de una colección que es mucho más pequeña. Otro
lugar donde esto puede dañar el rendimiento de una aplicación es durante la recolección de basura.
Esto es menos problemático si tenemos enormes colecciones que se asignan y nunca se liberan
(posiblemente debido a otros problemas como fugas). Pero el comportamiento más común es
asignar grandes cantidades de memoria en respuesta a una acción del usuario, para luego desasignar
esa memoria cuando el usuario sigue adelante. Este comportamiento desencadenará ejecuciones
frecuentes de recolección de basura, que se traduce en pausas en la respuesta.
La arquitectura de React requiere más memoria que los enfoques alternativos para memoria. Esto se
debe al DOM virtual que React mantiene. Esta estructura en memoria está pensada para reflejar la
estructura del DOM real. No rastrea cada dato únic sobre cada elemento que tiene el DOM real.
Sólo rastrea los datos necesarios para calcular las diferencias. He aquí una ilustración del mapeo
entre nuestro componente, el DOM virtual, y el DOM real:

Los elementos de nuestro componente React no necesariamente ocupan mucha memoria, porque
son sólo la parte declarativa del componente que especifica cuál elementos utilizar y qué valores de
propiedad deberían tener. El DOM virtual refleja la estructura y propiedades especificadas en
nuestro JSX; estos elementos en realidad ocupan la memoria. Por último, tenemos los elementos
DOM reales que el usuario ve y con los que interactúa. Estos ocupan una cantidad significativa de
memoria también.
El principal desafío con este enfoque es que estamos duplicando todo lo que está en el DOM. Dicho
de otra manera, el DOM virtual se añade a la memoria total consumida por nuestros elementos
DOM necesariamente. Sin el DOM virtual, React y JSX es sólo otro motor de plantillas. El DOM
virtual resuelve los problemas de rendimiento en otros lugares. El área principal en la que React
destaca por su rendimiento es la eficiente interacción con la API DOM, porque el DOM virtual
elimina la necesidad de muchas de estas llamadas.
¿Es la memoria consumida por una aplicación típica de React un obstaculo? Absolutamente no.
Tener grandes cantidades de memoria se está convirtiendo en una comodidad, incluso en los
móviles. Así que si puede asignar más memoria para resolver problemas de rendimiento real,
debemos hacerlo por supuesto. Sin embargo, hay casos en los que una asignación excesiva de
memoria puede convertirse en un problema con las aplicaciones de React. Por ejemplo, ¿qué pasa si
simplemente tenemos que renderizar un montón de elementos? Cuando, y si, esto se convierte en un
problema de rendimiento, su mejor apuesta es probablemente diseñar menos elementos.

JSX y lenguaje de marcas


JSX es esencialmente HTML (bueno, XML técnicamente) mezclado con código JavaScript. Si tu
respuesta inicial a este enfoque no fue favorable, no estás solo. Más de un largo período de tiempo,
décadas, en realidad, hemos estado condicionados a separar nuestros asuntos. Algo como HTML
nunca debería estar en el mismo módulo que la directiva lógica JavaScript que controla cuándo y
cómo se muestra ese lenguaje de marcas. Si hemos estado viviendo el principio de la separación de
preocupaciones durante tantos años, es natural oponerse a la noción de combinar la lógica en un
solo módulo.
Es muy posible que el último proyecto en el que trabajaste implicara la especificación del lenguaje
de marcas en plantillas. Estas plantillas se introducen en la capa de vista para su renderización.
Venir a Flux desde esta configuración puede ser demasiado para asimilarlo todo a la vez. Por un
lado, tenemos un nuevo flujo de datos unidireccional en el que pensar. Por otro lado, estamos
hablando de tirar todo lo que hemos trabajado tan duro para separar en capas.
No olvidemos el hecho de que el principio de separación de preocupaciones tiene un propósito. Si
dos preocupaciones se implementan en dos lugares diferentes, hay menos posibilidades de que el
cambio de una afecte a la otra. Piense en tener plantillas como una manera de compartimentar el
aspecto visual de cualquier componente dado. Podemos, al menos en teoría, dar al equipo de diseño
rienda suelta con las plantillas y no tener que preocuparse por ellas rompiendo la implementación
del componente JavaScript.
Si has aprendido algo hasta ahora en este libro, es probable que haya mucho más en lo que respecta
a las complejidades de los componentes de la interfaz de usuario que la suma de sus partes. Flux
intenta reconocer estas complejidades al modelarlas explícitamente en las stores. Hay un estricto
ordenamiento y sincronía en la actualización de la interfaz de usuario en Flux por una razón:
predecibilidad a pesar de todas las complejidad involucrada. ¿Qué tiene que ver esto con JSX?
Bueno, antes de que lo descontemos como algo que viola el principio de separación de
preocupaciones, piense sobre lo bien que encaja con las stores Flux. Considera también la idea de
que el lenguaje de marcas y la lógica que renderiza podría ser la misma preocupación después de
todo.

Bloqueo de proveedor
¿Alguna vez has oído a alguien decir algo como "estoy usando la biblioteca x porque no quiero
estar atado a la biblioteca y”? El bloqueo del vendedor es un área delicada para navegar. Aunque
en estos días, donde la mayoría de los proyectos dependen de proyectos de código abierto, es más
como un bloqueo tecnológico. Sería negligente si al menos no mencionara el tema aquí con respecto
a Flux y React.
Una vez que empezamos a usar React y JSX, hemos tirado los dados. Es una apuesta segura por
más razones de que sea inseguro. Sin embargo, hemos comenzado un camino que es muy difícil de
recorrer, que es esencialmente el punto de estas últimas tres secciones. Incluso si tu decisión de
elegir React es del 95%, dormirás mejor por la noche sabiendo que has sopesado las
compensaciones.

Uso de jQuery y Handlebars


Tanto jQuery como Handlebars son tecnologías omnipresentes en las aplicaciones web modernas.
Hay una alta probabilidad de que alguien nuevo en Flux haya usado uno o ambos de estos por lo
que pasaremos esta sección implementando algunas vistas que utilizan tanto jQuery y Handlebars.
Comenzaremos con una discusión sobre lo que hace que jQuery y Handlebars encajen bien para
implementar componentes de vista. Luego, implementaremos una vista básica que usa estas
tecnologías para renderizar el estado de las stores Flux. Después de esto, pensaremos en el varias
maneras en las que podemos componer vistas más grandes a partir de partes más pequeñas y la
mejor manera de gestionar eventos de usuario.

¿Por qué jQuery y Handlebars?


Antes de que existieran los frameworks JavaScript, existía jQuery. Esta es una pequeña biblioteca
para resolver problemas entre navegadores prevalentes en el desarrollo de frontend, y en general
para hacer el desarrollo más agradable. Hoy en día, jQuery sigue siendo un actor dominante entre
las bibliotecas de JavaScript. Muchos frameworks más grandes dependen de jQuery, porque es tan
eficaz y la curva de aprendizaje es muy baja.
Una cosa en la que jQuery no es tan bueno es especificar el diseño de los componentes de la
interfaz de usuario usando HTML. Por ejemplo, podemos usar jQuery para construir nuevos
elementos e insertar en el DOM sobre la marcha. Sin embargo, algo acerca de este enfoque se siente
incómodo y antinatural. A menudo es más claro poder escribir el HTML usando la misma estructura
que aparecería en la página. Esto elimina una capa de indirección y nos facilita la asignación del
lenguaje de marcas a la salida renderizada.
Entra Handlerbars. Esta librería añade un sofisticado motor de plantillas a nuestra interfaz. Escribir
plantillas de Handlebars significa que podemos escribir HTML, junto con algunas sintaxis
específica de Handlebars para los bits dinámicos, y evitar el lío de tratar de ensamblar elementos
utilizando jQuery. Ambas bibliotecas son complementarias entre sí. Tenemos plantillas de
Handlebars que declaran la estructura de nuestra aplicación, y usamos el motor de renderizado de
Handlebars para renderizar esta estructura. Entonces, jQuery puede manejar cualquier otro aspecto
de los componentes de la vista, como la selección de elementos DOM y el manejo de eventos.
Veamos cómo se ve esto en el contexto de una arquitectura Flux implementando una vista que
muestre una plantilla de Handlebars.

Renderización de plantillas
Empecemos por cubrir el escenario de uso más básico: renderizar una plantilla de Handlebars en un
elemento DOM utilizando jQuery. Comencemos primero por mirar el archivo de plantilla de
Handlebars:
<p><strong>First: </strong>{{first}}</p>
<p><strong>Last: </strong>{{last}}</p>
Como puedes ver, esto es esencialmente HTML básico con un poco sintaxis especial de Handlebars
para las partes dinámicas. Esta plantilla se almacena dentro de un archivo .hbs (abreviatura de
Handlebars: algunas personas utilizan la extensión completa .handlebars). Podemos actualizar
la configuración de nuestro Webpack para añadir el cargador de Handlebarse. Esto analiza y
compila las plantillas .hbs por nosotros, lo que significa que nuestro código que utiliza estas
plantillas puede ser importado como módulos JavaScript normales. Echemos un vistado a que
aparecería en nuestro componente vista:
// Importa la función compilada de la “template”
// Handlebars como si fuera un módulo JavaScript normal.
import template from './my-view.hbs';
import myStore from '../stores/my-store';
export default class MyView {
constructor(element) {
// Asigna el elemento contenedor que
// usaremos para poner el contenido renderizado
// de la plantilla. Se espera que sea un objeto jQuery.
this.element = element;
// Cuando el estado de la store cambia, podemos
// re-renderizar la vista.
myStore.on('change', (state) => {
this.render(state);
}
// Renderiza la vista. El estado por defecto es
// el inicial "myStore.state". Usamos la
// propiedad "element" de la vista para asignar el
// HTML a la salida renderizada de Handlebars
// "template()".
render(state = myStore.state) {
this.element.html(template(state));
return this;
}
}
La función template() que importa este módulo de vista se crea como resultado de el plugin de
Webpack que compila la plantilla en una función para nosotros. El tiempo de ejecución para
Handlebars se incluye como parte del paquete que crea Webpack. El render() de nuestro
componente vista llama a la función template(), pasándole un contexto y usando el valor de
retorno como nuevo contenido para el elemento de la vista. El contexto es sólo el estado de la store,
y cada vez que el estado de la store cambie, la función html() de jQuery se utiliza para
reemplazar el contenido del elemento existente.
La diferencia fundamental entre ReactJS y un enfoque como este que utiliza el motor
de plantillas Handlebars, es que React intenta hacer pequeñas actualizaciones. Con
Handlebars, podríamos terminar reemplazando mucho contenido DOM, y los
problemas de rendimiento pueden llegar a ser perceptibles para los usuarios. Para
combatir este tipo de problemas tenemos que cambiar la forma en que se compone
nuestra aplicación. Esto en sí mismo podría ponernos en desventaja en comparación
con el uso de algo como React donde podamos volver a renderizar grandes DOM y
seguir siendo eficientes.
Ahora, echemos un vistazo a la store que maneja el contenido de la vista:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { MY_ACTION } from '../actions/my-action';
// El estado inicial de la store. En lugar de
// cadenas vacías, este estado usa etiquetas que
// indican que aún hay datos por venir.
var state = {
first: 'loading...',
last: 'loading...'
};
class MyStore extends EventEmitter {
constructor() {
super();
this.id = dispatcher.register((e) => {
switch(e.type) {
// Cuando la acción ”MY_ACTION" es
// enviada, extendemos el estado
// con el valor del "payload",
// sobreescribiendo cualquier valor de propiedad existente.
case MY_ACTION:
this.emit('change',
(state = Object.assign(
{},
state,
e.payload
))
);
break;
}
});
}
get state() {
return state;
}
}
export default new MyStore();
Esta es una store bastante típica, no muy diferente a la mayoría de las stores que hemos visto hasta
ahora en este libro. El payload que se envía como parte de la acción MY_ACTION se utiliza para
ampliar el estado de la store y anulará los nombres de propiedades existentes, si los hubiera.
Echemos un vistazo al programa principal ahora:
import $ from 'jquery';
import { myAction } from './actions/my-action';
import MyView from './views/my-view';
// Construye la nueva vista y realiza el
// renderizado inicial llamando a "render()". Advierte
// que hay ahora una referencia almacenada a esta vista,
// porque actualmente no la necesitamos. Si la
// necesitáramos, "render()" devolvería la instancia de vista.
new MyView($('#app')).render();
// Tras un segundo, envía "MY_ACTION", la cual
// reemplazará la etiqueta "loading...".
setTimeout(() => {
myAction({
first: 'Face',
last: 'Book'
});
}, 1000);
Aquí es donde inicializamos la instancia de nuestro componente vista, pasándole una instancia
jQuery. Este objeto jQuery representa el elemento #app, y es usado por la vista para mantener el
contenido de la plantilla de Handlebars renderizada. Después de un segundo de retardo, llamamos a
myAction() , lo que hace que el estado de myStore cambie y la plantilla Handlebar se vuelva a
procesar.
En general, lo qué sucede cuando nuestras plantillas de Handlebars comienzan a
hacerse más grandes, empezaremos a añadir manejadores especializados que sólo
responder a propiedades específicas de la store. La razón es que las propiedades
cambian con demasiada frecuencia y sólo afectan a una pequeña sección de la UI
visible. Estos micro-manejadores entonces proliferan, y comenzamos a perder la
predecibilidad porque estamos introduciendo más caminos en el código de
renderizado. Con ReactJS, es menos probable que esto suceda, porque raramente
tenemos que descomponer nuestras vistas de esta manera.

Componer vistas
Si utilizamos plantillas de Handlebars como ingrediente principal de nuestros componentes vista,
probablemente necesitemos la habilidad de descomponer nuestras plantillas en trozos más
pequeños. Piensa en la forma en que descomponemos nuestros componentes de React: terminamos
con componentes más pequeños que normalmente se pueden compartir a lo larg de las
funcionalidades. Usando Handlebars podemos lograr algo similar usando plantillas parciales. Lo
parcial es una parte más pequeña que encaja en un todo más grande para formar la plantilla que se
renderiza por el componente de vista.
Comencemos observando una plantilla Handlebars que cumple como una vista con un listado para
una store que tiene un array de datos de usuario:
<ul>
{{#each users}}
<li>{{> item-view}}</li>
{{/each}}
</ul>
Esta plantilla está iterando sobre la propiedad users de nuestra store, que es un array. Sin
embargo, en lugar de renderizar directamente cada elemento, se refiere simplemente a una plantilla
parcial utilizando una sintaxis especial. Veamos esta plantilla parcial ahora, para que podamos tener
una idea de lo que se le está pasando:
<span style="text-transform: capitalize">{{first}}</span>
<span style="text-transform: capitalize">{{last}}</span>
En esta plantilla, no tenemos que calificar las propiedades que se utilizan en este caso: first y
last. El contexto en la plantilla padre se pasa a la plantilla parcial, en este caso, el objeto usuario.
Así que es como pasarle accesorios a un hijo de React desde un componente padre. Una vez más,
sin embargo, la diferencia es que cada componente de Handlebars que utilizamos para componer la
estructura de los elementos DOM se vuelve a renderizar ya que no hay un DOM virtual del que
hablar. Echemos un vistazo a la store que fue usada para rellenar esta vista con datos:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { REVERSE } from '../actions/reverse';
// El estado inicial es una lista de
// objetos usuario.
var state = {
users: [
{ first: 'first 1', last: 'last 1' },
{ first: 'first 2', last: 'last 2' },
{ first: 'first 3', last: 'last 3' }
]
};

class MyStore extends EventEmitter {


constructor() {
super();
this.id = dispatcher.register((e) => {
switch(e.type) {
// Cuando la acción "REVERSE" es enviada,
// el array "state.users" es invertido al
// llamar "reverse()".
case REVERSE:
this.emit('change',
(state = Object.assign(
{},
state,
{ users: state.users.reverse() }
))
);
break;
}
});
}
get state() {
return state;
}
}
export default new MyStore();
Y finalmente, el programa principal. Aquí, vamos a configurar un temporizador de intervalos que
sigue enviando la acción REVERSE. Esto hace que toda la interfaz de usuario se vuelva a procesar
con cada envío:
import $ from 'jquery';
import { reverse } from './actions/reverse';
import ListView from './views/list-view';
// Realiza el renderizado inicial de
// la vista lista, después de la inicializar
// la vista usando el elemento "#app".
new ListView($('#app')).render();
// Cada segundo, cambia el sentido
// de ordenación de la lista re-renderizando
// la plantilla principal y sus plantillas
// parciales.
setInterval(reverse, 1000);
En general, las arquitecturas Flux deben tener el menor número de stores posible.
Sin embargo, si utilizamos Handlebars en la capa de vista, es posible que nos
veamos influenciados en diseñar nuestras stores de forma diferente. Por ejemplo, es
posible que queramos dividir el estado de la aplicación colectiva de tal manera que
se vuelva a insertar menos estructura DOM en el documento.

Manejo de eventos
Mucho antes de que existieran los modernos frameworks web, jQuery abordaba los problemas de
gestión de eventos entre navegadores. Aunque la API ha cambiado a través de los años, las
poderosas capacidades del manejo de eventos de jQuery permanecen intactas. Esto es algo que
obviamente es relevante si estamos construyendo vistas que son potenciadas por jQuery y
Handlebars.
El reto más apremiante en el manejo de eventos en este contexto es el hecho de que estamos
volviendo a renderizar elementos cada vez que una plantilla de Handlebars necesita ser actualizada.
Lo que no queremos es tener que volver a conectar los manejadores de eventos a los elementos
DOM cada vez que se insertan en el DOM. ReactJS utiliza una estrategia que en realidad no enlaza
a los manejadores de eventos directamente con el elemento que queremos escuchar. En lugar de eso,
el manejador está ligado al elemento del cuerpo y a medida que los eventos afloran a la superficie,
el manejador apropiado es invocado. Resulta que este enfoque tiene una ventaja de rendimiento,
porque evita tener que enlazar la misma función de manipulación al mismo elemento una y otra vez.
He aquí una ilustración de la idea:

Podemos lograr algo similar usando jQuery. En primer lugar, echemos un vistazo a los archivos de
plantilla de Handlebar para que podamos hacernos una idea del tipo de interfaz de usuario con el
que estamos tratando aquí. Extenderemos el ejemplo anterior añadiendo un botón para invertir y
posibilidad de selección. Aquí está la nueva plantilla de vista:
<a href="#{{@index}}" style="font-weight: {{fontWeight}}">
<span style="text-transform: capitalize">{{first}}</span>
<span style="text-transform: capitalize">{{last}}</span>
</a>
El elemento es ahora un enlace. Tenga en cuenta que podemos utilizar la sintaxis @index de
Handlebars, que permite acceder al índice del ítem actual de la colección sobre la que estamos
iterando. Aunque la iteración ocurre en otra plantilla, este valor especial sigue siendo accesible.
Ahora veamos lo que tenemos en la vista lista principal de Handlebars:
<button>Reverse</button>
<ul>
{{#each users}}
<li>{{> item-view}}</li>
{{/each}}
</ul>
El ul que construye la lista es el mismo que antes. Ahora tenemos un nuevo botón para invertir el
orden de la lista, en lugar de un temporizador de intervalos. Ahora echemos un vistazo a las
funcionalidades de manejo de eventos del componente de vista:
import template from './list-view.hbs';
import { reverse } from '../actions/reverse';
import { select } from '../actions/select';
import myStore from '../stores/my-store';

export default class ListView {


constructor(element) {
this.element = element;
// Cuando la store cambia, re-renderiza
// la vista.
myStore.on('change', (state) => {
this.render(state);
});
this.element
// Enlaza el evento click a "#app", pero
// sólo es manejado is un elemento "button"
// generó el evento. La creadora
// de acciones “reverse()” es usada como manejadora.
.on('click', 'button', reverse)
// Enlaza el evento click a "#app", pero
// sólo es manejado si un elemento "a"
// generó el evento. El indice es resuelto
// desde un atributo "href", y este es
// pasado como payload a la creadora
// de acciones "select()".
.on('click', 'a', (e) => {
e.preventDefault();
let index = +(/(\d+)$/)
.exec(e.currentTarget.href)[1];
select(index);
});
}
// Asigna el HTML del "element" al renderizado
// "template()" de Handlebars. El contexto de
// la plantilla es siempre el estado de la store Flux.
render(state = myStore.state) {
this.element.html(template(state));
return this;
}
}
Estamos siguiendo el patrón de React donde el manipulador nunca está directamente conectado a
algo que se va a volver a renderizar con frecuencia. De hecho, puedes ver que los manejadores de
eventos se configuran en el constructor del componente vista, mucho antes de que nada de lo que ha
sido hecho por esta vista. Esto funciona porque el elemento #app ya está en marcha, y este es el
elemento que nos interesa.
El primer handler es para el botón reverse, y usa la función creadora de acciones reverse() . Es
el segundo parámetro a on() que proporciona el contexto del elemento, de manera que sabemos
que este manipulador es para elementos button. El mismo principio se aplica con nuestro
segundo handler, que se llama cuando el usuario hace clic en un enlace. Aquí, simplemente estamos
evitando el comportamiento predeterminado del navegador y enviando el evento select. Ahora,
echemos un vistazo a algunos de los cambios que tuvimos que hacer en nuestra store para apoyar
este nuevo comportamiento del evento:
import { EventEmitter } from 'events';
import dispatcher from '../dispatcher';
import { REVERSE } from '../actions/reverse';
import { SELECT } from '../actions/select';
// El estado inicial es un listado de
// objetos usuario. Cada uno tiene una
// propiedad"fontWeight" la cual es
// transladada como un valor CSS cuando
// renderizamos.
var state = {
users: [
{
first: 'first 1',
last: 'last 1',
fontWeight: 'normal'
},
{
first: 'first 2',
last: 'last 2',
fontWeight: 'normal'
},
{
first: 'first 3',
last: 'last 3',
fontWeight: 'normal'
}
]
};
class MyStore extends EventEmitter {
constructor() {
super();
this.id = dispatcher.register((e) => {
switch(e.type) {
// Cuando la acción "REVERSE" es enviada,
// el array "state.users" es invertido
// llamando "reverse()".
case REVERSE:
this.emit('change',
(state = Object.assign(
{},
state,
{ users: state.users.reverse() }
))
);
break;
// Cuando la acción "SELECT" es enviada, necesitamos
// encontrar el objeto apropiado basado en
// el indice "payload" y marcándolo como seleccionado.
case SELECT:
this.emit('change',
(state = Object.assign(
{},
state,
{ users: state.users.map((v, i) => {
// Si el indice actual es el objeto
// seleccionado, cambia la propiedad
// "fontWeight".
if (i === e.payload) {
return Object.assign({}, v,
{ fontWeight: 'bold' });
// De otra manera, asigna el
// "fontWeight" de vuelta
// a "normal" así cualquier objetjo
// seleccionado previamente es reseteado.
} else {
return Object.assign({}, v,
{ fontWeight: 'normal' });
}
})}
))
);
break;
}
});
}
get state() {
return state;
}
}
export default new MyStore();
Hay dos cambios importantes que vale la pena señalar. El primer cambio es que nuestra matriz
users ahora tiene una nueva propiedad fontWeight para cada elemento dentro de ella. Esto es
necesario porque controla la visualización de nuestros enlaces para indicar que algo ha sido
seleccionado. Todo es normal por defecto, ya que todavía no se ha seleccionado nada.
Podríamos poner algún código en nuestro componente de vista que busque una
propiedad fontWeight, y cuando no pueda encontrar una, por defecto sea normal.
El problema con esta táctica es que introduce lógica innecesaria en el componente de
vista. Tratamos de mantener todo en la store, incluso cosas aparentemente triviales
como esta. Incluso si eso significa añadir valores por defecto en una store que
también están por defecto en el navegador.
El segundo cambio en la store es la adición de la lógica de manejo SELECT. Cuando esta acción es
enviada, compararemos el índice del objeto con el índice del payload y cambiamos el tamaño de la
fuente. Todo lo demás que no coincida se revierte a un font­weight normal.

Usando VanillaJS
No tener suficiente diversidad en el ecosistema de renderizado JavaScript de frontend no es un
problema. De hecho, el problema para nosotros es exactamente lo contrario – hay muchas
bibliotecas y frameworks para elegir. Mientras que algunas personas en la comunidad JavaScript ve
esta plétora desarticulada de opciones como un problema, no tiene por qué serlo. Es mejor tener
demasiadas tecnologías para elegir que no tener suficientes.
En esta sección, discutiremos el uso de VanillaJS como nuestra tecnología de visualización-sin
bibliotecas o frameworks. La idea no es evitar completamente el uso de frameworks, es mantener
nuestras opciones abiertas a medida que se desarrolla la arquitectura de nuestra aplicación.
Eventualmente, podríamos mover nuestros componentes de vista para usar React, o tal vez hay
alguna otra novedad a la que le teníamos echado el ojo.

Mantener mis opciones abiertas


En algún momento, tenemos que elegir una tecnología para usar con nuestros componentes de
visualización. Eso depende de la etapa del proyecto en la que estemos. Si es al inicio y ya nos
hemos decidido por una biblioteca de vistas, podríamos acabar limitándonos a esta tecnología
durante mucho tiempo. Dada la velocidad a la que JavaScript y el ecosistema que lo rodea se está
moviendo, estar atascado con cualquier tecnología no es una buena opción. Tenemos que aceptar el
hecho de que el cambio está constantemente despreciando las cosas que una vez fueron atractivas.
Por otro lado, no queremos esperar demasiado para tomar una decisión tecnológica para nuestras
vistas, porque mientras más cosas construimos usando JS simple, más difícil va a ser para migrar
estas vistas a un enfoque más dogmático. ¿Dónde está el punto medio?
La mejor estrategia es evitar el bloqueo cuando sea posible. Esto implica mantener las cosas para
que sean sustituibles. Afortunadamente, las arquitecturas de Flux hace esto fácil porque las
responsabilidades de la capa de vista son bastante limitadas. Necesitan escuchar los eventos de
cambio de stores y mostrar el estado de stores. Tal vez deberíamos intentar la construcción de dos
conjuntos de componentes vista. El primer conjunto utiliza una tecnología como React, y el otro usa
algo más, como jQuery y Handlebars. Esto no sólo nos permite elegir la tecnología de visualización
que mejor se adapte a nuestro producto, sino que también nos permite probar nuestra disposición a
adoptar nuevas tecnologías, lo que inevitablemente querremos hacer.
Mudarse a React
Como se ha visto en este capítulo, podemos utilizar tecnologías como jQuery y Handlebars en los
componentes de vista de nuestra arquitectura Flux. Lo que es más, no interfieren con el flujo de
datos unidireccional encontrado en las arquitecturas Flux. Dicho esto, React es probablemente la
tecnología de visualización más adecuada para usar como parte de una arquitectura Flux. Desde la
perspectiva del flujo de datos unidireccional, React lo capta de forma natural. Incluso sin Flux, los
componentes funcionales sin estado de React se comportan exactamente como nosotros
esperaríamos que una vista se comportara en una arquitectura Flux. Cuando llegan nuevas
propiedades, se renderiza el nuevo HTML.
Además de la tendencia natural de React hacia el flujo de datos unidireccional, la idea de volver a
renderizar grandes estructuras DOM se siente menos intimidante. Gracias al DOM virtual que React
usa para parchear la salida renderizada, en lugar de reemplazarlo todo, podemos pasar
eficientemente el estado de la store a las vistas de nivel superior para volver a renderizar. React
también maneja otros casos límite por nosotros, como mantener el foco de un control de formulario
durante un nuevo renderizado.
La verdadera pregunta es doble: ¿cuán inevitable es mudarse a React, y cuán salvable es nuestro
código actual? Bueno, la primera pregunta es generalmente bastante fácil de responder. hay una alta
probabilidad de que vaya a utilizar React en su arquitectura Flux. Es simplemente un buen ajuste
para una arquitectura Flux. Sin embargo, es ingenuo asumir que no hay compensaciones negativas,
como, por ejemplo, un mayor consumo de memoria. Así que si deciden pasar a React después de
haber desarrollado algunos componentes de vista, ¿Tenemos que tirar todo? Es poco probable. Las
vistas juegan un papel relativamente pequeño en arquitecturas Flux, como hemos enfatizado a lo
largo del libro. Así que, si mudarnos a React resuelve problemas en sus componentes de vista Flux,
por supuesto que sí, es una buena dirección a tomar. Por ahora.

Novedades
Hace un par de años, React era una novedad. Como con cualquier otra novedad, los desarrolladores
pueden y deben acercarse a la tecnología con cierto grado de escepticismo. Resultó que React fue
una buena apuesta para muchos de sus primeros usuarios. Por otro lado mano, no todas las nuevas y
brillantes tecnologías funcionan. Así es como se avanza, y es por eso que se ha progresado tanto en
el ecosistema JavaScript. ¿Cual es la moraleja? Siempre va a haber una novedad que sea superior a
lo que ya has apostado. Prepárate para adoptar y volver a adoptar. Por ejemplo, Google está
implementando actualmente una tecnología de visualización llamada Incremental DOM
(http://google.github.io/incremental­dom/), que requiere un uso de mucha menos
memoria. Está Vue.js (http://vuejs.org/). Hay un sinfín de otras posibilidades futuras.
Sólo asegurate que tus vistas pueden cambiar y abrazar la última y mejor tecnología para vistas –
que pronto estará aquí.

Sumario
El enfoque de este capítulo se centró en los componentes de vista de nuestra arquitectura Flux y
cómo están sueltos hasta el punto de que podemos sustituirlos por otras tecnologías de vista.
Empezamos con una discusión sobre React en sí y qué lo convierte en adecuado para arquitecturas
Flux. Luego, cambiamos de marcha y cubrimos las potenciales desventajas del uso de ReactJS.
Pasamos algún tiempo implementando vistas que aprovechaban tanto a jQuery como a Handlebars.
Estas son dos tecnologías maduras con las que muchos desarrolladores están familiarizados y sirven
como un buen punto de partida para implementar una arquitectura Flux. Sin embargo, hay fuertes
motivaciones para cualquiera que implemente Flux en mirar React como la tecnología de vista de su
elección.
Concluimos el capítulo con una discusión sobre el uso de VanillaJS para mostrar nuestros
componentes vista. No tiene sentido apresurarse a usar una tecnología en particular hasta que
entendemos las ramificaciones de esa elección. Siempre va a haber más bibliotecas nuevas y
mejores de vistas, y las arquitecturas de Flux hacen que sea fácil de cambiar y abrazar una novedad.
12
Aprovechamiento de las librerías Flux
Flux, ante todo, es un conjunto de pautas arquitectónicas, especificadas como patrones para que los
sigamos. Si bien esto proporciona la máxima flexibilidad, puede ser paralizante a veces, cuando se
trata de decidir cómo implementar un componente Flux concreto. Afortunadamente, hay algunas
bibliotecas de Flux realmente buenas por ahí que proporcionan implementaciones dogmáticas de
componentes de Flux, que eliminan la necesidad de una gran cantidad de del código que tendríamos
que escribir. La idea de este capítulo es mirar en dos de estas librerías, para mostrar lo diferentes
que pueden ser las implementaciones de Flux. El objetivo no es la conformidad, sino una
arquitectura sólida que ayude a nuestra aplicación a hacer el trabajo.

Implementación de componentes principales de Flux


En esta sección, vamos a reiterar la idea de que podemos cambiar los detalles de implementación de
los distintos componentes de Flux en nuestra arquitectura. Comenzaremos hablando sobre el propio
dispatcher, y pensaremos en los diversos cambios que podríamos hacer. Luego, pensaremos en las
stores y en las mejoras que podríamos hacer allí. Finalmente, discutiremos las acciones y las
funciones de creación de acciones.

Personalización del dispatcher


En el Capítulo 10, Implementación de un dispatcher, implementamos nuestro propio componente
dispatcher. La implementación de referencia por parte de Facebook está perfectamente bien, pero no
está pensada para ser el componente de facto que se encuentra en todas las arquitecturas de
producción de Flux. En su lugar, se supone que es un punto de partida, por lo que podemos ver
cómo se supone que funciona la especificación de un dispatcher Flux.
Nuestra solución fue exponer las funciones dispatch() y register() desde el módulo
disptacher. Al hacer esto, hicimos al dispatcher un poco más directo en otras áreas de nuestro
código. Ya no había una instancia de dispatcher en la que pensar, todo estaba encapsulado dentro del
módulo dispatcher.
Una librería Flux genérica podría querer llevar esto un paso más allá y disolver completamente al
dispatcher, lo que puede parecer una locura, en un componente esencial Flux. Sin embargo, todavía
podemos lograr el mismo principio arquitectónico del dispatcher sin implementar explícitamente
esta abstracción. Este es el punto de liberar a Flux como un conjunto de especificaciones en lugar de
una implementación concreta. Sabemos conceptualmente qué debe hacer y qué no debe hacer una
arquitectura Flux: nosotros elegimos cómo hacer cumplir estas reglas en nuestra implementación.

Implementación de un store base


Otra mejora que hicimos en el Capítulo 10, Implementación de un Dispatcher, fue en la jerarquía de
las stores. Hicimos que cada una de nuestras stores heredara de una clase base. La razón principal
por la que implementamos esta funcionalidad fue para automatizar el registro de la store con el
dispatcher, lo que es bueno, porque no tiene mucho sentido en una store Flux que no está
escuchando los eventos emitidos por el dispatcher. Quizás una librería Flux debería manejar este
tipo de funcionalidad base a nosotros.
También implementamos manejadores de métodos de acción. Esto fue en realidad una función
disptacher en sí en nuestra implementación, y fue bastante limitante. Tal vez la store base es el lugar
adecuado para este tipo de funcionalidad. Las bibliotecas deben contener este tipo de complejidad
genérica, no nuestra aplicación.
Lo bueno de heredar la funcionalidad base con las stores Flux es que esto es donde viven los
cerebros de nuestra aplicación. Si descubriéramos algún estado genérico que se aplica a más de una
store, teniendo un store base hace que sea fácil para nosotros factorizar el código común. Tal vez
una librería Flux podría lidiar con algunas transformaciones básicas en su store base de las que
heredaríamos.

Creación de acciones
Las constantes son una gran manera de ser explícito sobre las acciones en las arquitecturas de Flux.
El módulo de acción define la constante, y la función creadora de acción pasa la constante al
dispatcher. Las stores también utilizan estas constantes para determinar cómo manejar las acciones a
medida que se envían. Esto crea un vínculo explícito entre la creadora de la acción y el código en
las stores que responden a esta acción.
En el Capítulo 10, Implementación de un Dispatcher, adoptamos un enfoque diferente. Las
funciones creadoras de acciones siguen definiendo constantes y las utilizan al planificar la acción.
Sin embargo, hicimos cambios que permitieron a nuestras stores definir los manejadores de
métodos. Así que en lugar de una función que escuchaba al dispatcher, las stores definían métodos
que coincidían con la constante definida para la acción. Esto es conveniente desde la perspectiva de
store, pero disminuye el valor de tener constantes si sólo son utilizadas por las funciones creadoras
de acciones.
Una librería Flux podría ayudar a hacer que las acciones de envío y manejo sean un poco más
sencillas. El uso de constantes y sentencias switch es bueno en la medida en que hace explícito a
lo que está pasando. Nos gusta la claridad en nuestra arquitectura Flux. El reto es que este enfoque
requiere diligencia por parte de los programadores que implementan el sistema. En otras palabras,
hay muchas oportunidades para el error humano. Flux podría eliminar los aspectos propensos a
errores al tratar con constantes en dos lugares.
Otra área en la que una librería Flux podría ayudar es con las funciones asíncronas creadoras de
acciones. El comportamiento asíncrono de nuestra aplicación es probable que siga un patrón
similar:
• Envía una acción que modifique el estado de una store antes de la ejecución de código
asíncrono.
• Evía una acción cuando llega la respuesta.
• Envía una acción diferente si el comportamiento asíncrono falla.
Es casi como si las acciones asíncronas tuvieran un ciclo de vida que podría ser abstraído en una
función común de una librería Flux.
Puntos débiles de la aplicación
En la sección anterior, cubrimos las áreas de Flux que se podrían beneficiar de la implementación
de una solución personalizada. Antes de zambullirnos en Alt.js y Redux, hablaremos brevemente
sobre algunos puntos débiles de la implementación de arquitecturas Flux. Las acciones asíncronas
son difíciles para hacer lo correcto, en cualquier arquitectura, por no hablar de Flux. La forma en
que particionamos nuestra aplicación en las stores puede ser un problema de diseño delicado. Si nos
equivocamos, puede ser difícil de recuperar. Finalmente, tenemos que pensar en los desafíos de la
dependencia de datos.

Planificación de acciones asíncronas


Como hemos discutido en la sección anterior, los creadores de acciones asíncronas son difíciles de
implementar. Es un reto porque normalmente tenemos que hacer saber a las stores que esta acción
asíncrona está a punto de tener lugar para que la interfaz de usuario pueda actualizarse para
reflejarlo. Por ejemplo, cuando se hace clic en un botón que envía una o más peticiones AJAX,
nosotros probablemente querremos deshabilitar ese botón antes de enviar la petición, para evitar
solicitudes duplicadas. La única manera de hacer esto en Flux es enviar una acción, porque todo es
unidireccional.
Las bibliotecas pueden ayudar con esto, hasta cierto punto. Por ejemplo, las acciones previas a la
solicitud y las acciones de respuesta al éxito/error pueden ser algo abstractas en algo que es más
fácil de usar, porque es un patrón común. Sin embargo, incluso haciendo esto deja la cuestión de
reunir peticiones para ir a buscar todos los datos que se necesitan para una acción determinada,
sincronizar las respuestas y pasarlas a la store para que puedan transformarse en algo que la vista
necesita.
Tal vez sea mejor si dejamos este problema de asincronía fuera del ámbito de Flux. Facebook ha
introducido GraphQL, por ejemplo, un lenguaje que simplifica esto creando datos complejos a
partir de servicios de backend y respondiendo sólo con los que la store realmente necesita. Todo
esto se hace en una sola respuesta, por lo que ahorramos en ancho de banda, y latencia también.
Este enfoque no es para todos, así que depende de Flux el elegir cómo quiere lidiar con la
asincronía, siempre y cuando el flujo de datos unidireccional en el cliente permanezca intacto.

Partición de stores
La partición incorrecta de las stores en nuestra arquitectura Flux es quizás uno de los problemas
más comunes. mayores riesgos de diseño a los que nos enfrentamos. Lo que sucede generalmente es
que las stores son más o menos equilibradas; entonces, a medida que el sistema evoluciona, todas
las nuevas características terminan en una sola mientras que las responsabilidades de las otras stores
no quedan claras. Las stores se desequilibran, en otras palabras. El estado de la store que mantiene
la mayor parte de la aplicación se vuelve demasiado complejo para mantenerlo.
Otro problema potencial con la partición de nuestras stores es que crecen siendo demasiado
detalladas. Tampoco queremos que esto suceda. Aunque el estado que ha logrado por stores
individuales es bastante simple, la complejidad reside en las dependencias entre todas estas stores.
Incluso si no hay demasiadas dependencias, cuando hay más stores en las que pensar, es más difícil
recordar suficientes estados así como tratar de razonar sobre algo. Cuando el estado relacionado
está todo en un mismo lugar, es mucho más fácil de predecir lo que sucederá.
¿Qué pasaría si una librería Flux, como Redux, adoptara un enfoque radical y eliminara todas las
fuentes confusas permitiendo una sola store? De hecho, esto evita problemas de diseño como la
partición de las stores. En cambio, como veremos más adelante en el capítulo, Redux usa funciones
reductoras para transformar el estado de la única store.

Usando Alt
Alt.js es una librería Flux que implementa mucho del código repetitivo por nosotros. Se adhiere
completamente a los conceptos y patrones de Flux, pero enfoquémonos en la arquitectura desde la
perspectiva de nuestra aplicación, en lugar de preocuparnos sobre constantes de acción y sentencias
de cambio.
En esta sección, tocaremos los conceptos básicos de Alt antes de sumergirnos en un simple ejemplo
de lista de cosas por hacer. El ejemplo es intencionalmente simple - usted podrá observar el código
volviendo a los conceptos Flux que has aprendido hasta ahora en este libro.

Las ideas centrales


El objetivo principal del paquete Flux de Facebook es proporcionar una implementación de
referencia de un componente básico del dispatcher. Esto sirve como ayuda para los conceptos de
Flux-accienos que se envían a las stores de forma sincrona y unidireccional. Como hemos visto a
través del libro, el concepto de dispatcher ni siquiera tiene que ser necesariamente expuesto a
aquellos que están implementando Flux. Podemos simplificar las abstracciones de Flux y aún así
caen dentro de las restricciones de una arquitectura Flux.
Alt es una librería Flux que se supone que debe ser usada en aplicaciones de producción - no es una
librería de implementación de referencia. Repasemos algunos de sus objetivos como biblioteca Flux
antes de saltar al código.
• Obediente: Alt no toma ideas prestadas de Flux-está realmente dirigido a sistemas Flux. Por
ejemplo, el concepto de stores, acciones y vistas son relevantes. Asimismo, los principios de
la arquitectura Flux son seguido muy de cerca por Alt. Cosas como la rondas de
actualización asíncrona y el flujo de datos unidireccional.
• Automatiza las tareas repetitivas: Algunas de las tareas de programación más tediosas
asociadas con la implementación de Flux son manejadas muy bien por Alt. Entre otras cosas
incluye la creación automática de funciones creadoras de acción y constantes de acción. Alt
también se encargará de los métodos de manejo de acciones de la store por nosotros-
reduciendo la necesidad de largas declaraciones switch.
• No hay dispatcher: No hay ningún dispatcher con el que nuestro código pueda interactuar.
El envío de las acciones a todas las stores se realiza entre bastidores, cuando llamamos a las
funciones creadoras de acciones. Cosas como la gestión de las dependencias entre tiendas se
gestionan directamente dentro de las propias stores.

Creación de stores
La sencilla aplicación que vamos a crear mostrará dos listas para el usuario. Una lista es para los
ítems que "hacer", la otra lista es para los ítems que han sido completados. Usaremos dos stores,
una para cada lista. Echemos un vistazo a cómo creamos stores usando Alt.js. Primero, tenemos
la store Todo:
import alt from '../alt';
import actions from '../actions';
class Todo {
constructor() {
// Este es el estado del elemento input
// usado para crear un nuevo objeto Todo.
this.inputValue = '';
// La lista inicial de objetos todo...
this.todos = [
{ title: 'Build this thing' },
{ title: 'Build that thing' },
{ title: 'Build all the things' }
];
// Establece el manejador de métodos a ser llamado
// cuando la acción correspondiente es enviada.
this.bindListeners({
createTodo: actions.CREATE_TODO,
removeTodo: actions.REMOVE_TODO,
updateInputValue: actions.UPDATE_INPUT_VALUE
});
}
// Crea un nuevo Todo usando el payload de acción
// como title.
createTodo(payload) {
this.todos.push({ title: payload });
}
// Elimina el Todo basandose en el indice, el cual es
// pasado en el payload de acción.
removeTodo(payload) {
this.todos.splice(payload, 1);
}
// Actualiza el valor Todo que el usuario está actualmente
// introduciendo en la caja input Todo.
updateInputValue(payload) {
this.inputValue = payload;
}
}
// La función "createStore()" engancha nuestra clase store
// con toda estructura de acción relevante enviada,
// devolviendo una instancia de la store.
export default alt.createStore(Todo, 'Todo');
Esto probablemente no se ve muy familiar, en relación con lo que hemos visto hasta ahora en este
libro. No te preocupes, pasearemos a través de las partes móviles aquí y ahora. La primera pregunta
que probablemente tienes es, ¿dónde está el estado? No está claro si miramos el código, pero el
estado es cualquier variable de instancia de la clase. En este caso, es el string inputValue y el
array todos.
A continuación, tenemos una llamada a bindListeners() con un objeto de configuración
pasado a ella. Así es como Alt almacena las acciones en los métodos. Puedes ver que tenemos
métodos definidos que corresponden a lo que se pasa a bindListeners() . Por último, tenemos
la llamada a createStore() . Esta función instancia la clase de store Todo por nosotros, pero
también conecta el mecanismo de envío.
Eso es todo lo que hay en la definición de la store: está lista para ser utilizada por las vistas que la
necesitan para mostrar su estado. Ahora echemos un vistazo a la tienda Done, que sigue el mismo
camino sólo con menos piezas móviles:
import alt from '../alt';
import actions from '../actions';
import todo from './todo';
class Done {
constructor() {
// El estado "done" contiene un array de
// objetos completados.
this.done = [];
// Enlaza el único listener de esta store.
this.bindListeners({
createDone: actions.CREATE_DONE
});
}
// Este payload de acción es el indice de un objeto
// desde la store "todo". Este es llamado cuando
// el objejot es clicado, y el objeto es añadido
// al array "done".
//
// Advierte que este manejador de acción no cambia
// el estado "todo" porque no está permitido.
createDone(payload) {
const { todos } = todo.getState();
this.done.splice(0, 0, todos[payload]);
}
}
// Crea la instancia de store, y la engancha
// con la estructura de envío de Alt.
export default alt.createStore(Done, 'Done');
Puedes ver aquí que esta store utiliza realmente la store Todo para copiar los datos de posición
cuando un artículo está marcado como hecho. Sin embargo, esta store no muta la store Todo, ya
que eso violaría el flujo de datos unidireccional.
Estas clases store no son emisoras de eventos, por lo que no emiten nada
explícitamente cuando cambia el estado. Por ejemplo, cuando se añade un todo,
¿cómo saben las vistas que algo ha cambiado? Dado que el método
createTodo() se llama automáticamente por nosotros, el mecanismo de
notificación también ocurre automáticamente una vez que nuestro método ha
terminado de ejecutarse. Veremos más sobre la semántica de notificación de
cambios de estado en unos momentos.

Declarar creadoras de acción


Hemos visto cómo las stores responden a las acciones que se envían. Ahora necesitamos un medio
para enviar estas acciones. Este es probablemente el aspecto más fácil de nuestra aplicación Alt. Alt
puede generar las funciones que necesitamos, así como las constantes que utiliza la llamada
bindListeners() en nuestras stores. Echemos un vistazo al módulo de acciones y veamos
cómo funciona con Alt:
import alt from './alt';
// Exporta un objeto con las funciones que aceptan
// un argumento payload. Estas son las creadoras
// de acción. También, crea las constantes de acción
// basado en los nombres pasados a "generateActions()"
export default alt.generateActions(
'createTodo',
'createDone',
'removeTodo',
'updateInputValue'
);
Esto exportará un objeto con funciones creadoras de acciones que tienen los mismos nombres que la
directiva pasadas a generateActions() . Y generará las constantes de acción usadas por la
store. Como nuestras funciones creadoras de acciones son todas muy similares,
generateActions() tiene una gran utilidad. Hay mucho código que ya no tenemos que
mantener. Por otro lado, hay casos más complejos que involucran acciones asíncronas que necesitan
más código que este. Eche un vistazo a la documentación de Alt para acciones asíncronas si estas
interesado en utilizar esta biblioteca para tu proyecto.

Escuchar los cambios de estado


A lo largo de este libro, hemos añadido funciones de gestión de eventos al evento de cambio
emitido por nuestras stores. Con bibliotecas como Alt, esto ya está gestionado por nosotros.
Echemos un vistazo al módulo principal de nuestra aplicación que utiliza el componente
AltContainer React para introducir datos de almacenamiento en nuestros otros componentes de
React:
// Los componentes React y Alt que necesitamos...
import React from 'react';
import { render } from 'react-dom';
import AltContainer from 'alt-container';
// Las stores y componentes React de
// esta aplicación...
import todo from './stores/todo';
import done from './stores/done';
import TodoList from './views/todo-list';
import DoneList from './views/done-list';
// Renderiza el componente "AltContainer". Aquí
// es dónde las stores son atadas a las vistas.
// Los componentes "TodoList" y "DoneList"
// son hijos de AltContainer, así
// obtienen las stores "todo" y "done"
// como propiedades.
render(
<AltContainer stores={{ todo, done }}>
<TodoList/>
<DoneList/>
</AltContainer>,
document.getElementById('app')
);
El componente AltContainer acepta una propiedad de store. El contenedor escuchará a cada
una de estas stores y volverá a procesar a sus hijos cuando cambie el estado de la store. Esta es la
única configuración necesaria para conseguir que nuestras vistas escuchen a las stores, sin llamadas
manuales a on() o listen() por todos lados. En la siguiente sección, veremos los componentes
TodoList y DoneList para ver cómo funcionan con AltContainer .

Visualización de vistas y envío de acciones


El trabajo del componente TodoList es renderizar elementos de la store Todo. Hay otras dos
cosas que esta vista debe manejar también. En primer lugar, está el elemento de entrada que el
usuario utiliza para introducir nuevos elementos todo. En segundo lugar, también necesitamos
marcar los elementos como hechos cuando se hace clic en ellos, moviéndolos a la lista de hechos.
Estas dos últimas responsabilidades implican la gestión de eventos y las acciones de envío.
Echemos un vistazo a la implementación de la vista de lista de tareas pendientes:
import React from 'react';
import { Component } from 'react';
import actions from '../actions';
export default class TodoList extends Component {
render() {
// El estado relevante de la store "todo"
// que hemos renderizado aquí.
const { todos, inputValue } = this.props.todo;
// Renderiza un input para nuevos todos, y la lista
// de los actuales todos. Cuando el usuario escribe
// y pulsa enter, el nuevo todo es creado.
// Cuando el usuario cliquea un todo, se mueve a la
// store "done".
return (
<div>
<h3>TODO</h3>
<div>
<input
value={inputValue}
placeholder="TODO..."
onKeyUp={this.onKeyUp}
onChange={this.onChange}
autoFocus
/>
</div>
<ul>
{todos.map(({ title }, i) =>
<li key={i}>
<a
href="#"
onClick={this.onClick.bind(null, i)}
>{title}</a>
</li>
)}
</ul>
</div>
);
}
// Un activo Todo fue clicado. La "key" es el
// indice del Todo en la store. Este es
// pasado como a la acción
// "createDone()", y seguido a la acción "removeTodo()".
onClick(key) {
actions.createDone(key);
actions.removeTodo(key);
}
// Si el usuario introdujo algún texto y la
// tecla "enter" es presionada, usamos la
// acción "createTodo()" para crear un nuevo
// elemento usando el texto introducido. Entonces limpiamos
// el input usando la acción
// "updateInputValue()", pasandole un string vacío.
onKeyUp(e) {
const { value } = e.target;
if (e.which === 13 && value) {
actions.createTodo(value);
actions.updateInputValue('');
}
}
// El valor del text input cambió – actualiza la store.
onChange(e) {
actions.updateInputValue(e.target.value);
}
}
Tal vez te estés preguntando por qué no podemos borrar e.target.value
cuando se pulsa la tecla Enter. De hecho, podríamos hacerlo, pero esto iría en contra
de la naturaleza de Flux, donde el estado se mantiene en los stores. Esto incluye
valores transitorios a medida que el usuario los introduce. ¿Qué pasa si otra parte de
la aplicación quiere saber sobre el valor de la entrada de texto? Bueno, todo lo que
necesita es depender de la store Todo. Si el estado no estuviera allí, entonces nuestro
código tendría que consultar al DOM, lo cual no queremos hacer.
Finalmente, echemos un vistazo al componente del listado de hechos. Este componente es más
sencillo que el listado de todo porque no tiene manejadores de eventos:
import React from 'react';
import { Component } from 'react';
export default class DoneList extends Component {
render() {
// El array "done" es el único estado que necesitamos
// del store "done".
const { done } = this.props.done;
// Queremos mostrar estos elementos
// texto tachado.
const itemStyle = {
textDecoration: 'line-through'
}
// Renderiza el listado de elementos hechos, con
// el "itemStyle" aplicado a cada elemento.
return (
<div>
<h3>DONE</h3>
<ul>
{done.map(({ title }) =>
<li style={itemStyle}>{title}</li>
)}
</ul>
</div>
);
}
}

Usando Redux
En esta sección, vamos a ver la librería Redux para implementar una arquitectura Flux. A diferencia
de Alt.js, Redux no se suma al cumplimiento de Flux. El objetivo de Redux es tomar prestadas
las ideas importantes de Flux, dejando atrás las partes tediosas. A pesar de no implementar los
componentes de Flux como se especifica en la documentación oficial, Redux es la solución ideal
para las arquitecturas de React. Redux es la prueba de que la simplicidad siempre gana sobre las
funcionalidades avanzadas.

Las ideas centrales


Antes de implementar algún código de Redux, tomémonos un momento para ver las ideas centrales
de Redux:
• No hay dispatcher: Esto es como Alt.js, que también elimina el dispatcher de su API. El
hecho de que estas librerías Flux no exponga un dispatcher sirve para ilustrar el punto de
que Flux es sólo un conjunto de ideas y patrones, no una implementación. Los envíos en Alt
y Redux no requiren de un dispatcher que los haga.
• Una store para gobernarlas a todas: Redux evita la noción de que una arquitectura Flux
requiera múltiples stores. En su lugar, una store se utiliza para contener todo el estado de la
aplicación. A primera vista, esto podría sonar como que la store sería demasiado grande y
difícil de entender. Esto es igual de probable que ocurra si se tienen múltiples stores, la única
diferencia es que el estado de la aplicación es dividido en distintos módulos.
• Envío a la store: Cuando sólo hay una store de la que preocuparse, podemos hacer
concesiones de diseño, como tratar la store y el dispatcher como el mismo concepto. Esto es
exactamente lo que hace Redux: envía las acciones directamente a la store.
• Reductoras puras: La idea detrás de múltiples stores Flux es dividir el estado de la
aplicación en unos pocos dominios separados lógicamente. Todavía podemos hacer esto
usando Redux, la diferencia es que separamos nuestro estado en dominios usando funciones
reductoras. Estas funciones son las encargadas de transformar el estado de la store cuando se
envía acciones. Son puras porque devuelven nuevos datos y evitan la introducción de efectos
secundarios.

Reductoras y stores
Ahora vamos a implementar la misma aplicación simple de todo que hicimos usando Alt, esta vez
usando Redux. Hay mucha equivalencia entre las dos bibliotecas, especialmente con los propios
componentes de React; no hay mucho que cambiar allí. Donde Redux se aparta de Alt y Flux en
general es con su única store, y las funciones reductoras que cambian su estado. Con esto dicho,
miraremos en la store y sus funciones reductoras primero.
Crearemos un módulo para el estado inicial de la store Redux. Este es un importante primer paso
porque proporciona la estructura inicial para las funciones reductoras que transforman el estado de
la store. Echemos un vistazo al módulo de estado inicial:
import Immutable from 'immutable';
// El estado inicial de la store Redux. La
// "forma" del estado de la aplicación incluye
// dos dominios - "Todo" y "Done". Cada dominio
// es una estructura Immutable.js.
const initialState = {
Todo: Immutable.fromJS({
inputValue: '',
todos: [
{ title: 'Build this thing' },
{ title: 'Build that thing' },
{ title: 'Build all the things' }
]
}),
Done: Immutable.fromJS({
done: []
})
};
export default initialState;
El estado es un simple objeto JavaScript. Puedes ver que la store no es sólo un enredo de
propiedades, sino que está organizada por dos propiedades principales - Todo y Done . Esto es
como tener múltiples stores, excepto que están en un solo objeto. Otra cosa que notarás es que cada
propiedad de la store es una estructura de datos Immutable.js. La razón de esto es que
necesitamos tratar el estado que ha pasado a nuestras funciones reductoras como inmutable. Esta
biblioteca facilita la aplicación de la inmutabilidad.
Las transformaciones de estado que tienen lugar con el estado de la store se dividirán en dos
funciones reductoras. De hecho, las dos funciones se asignan a las dos propiedades iniciales de la
store: Todo y Done . Veamos primero la reductora Todo:
import Immutable from 'immutable';
import initialState from '../initial-state';
import {
UPDATE_INPUT_VALUE,
CREATE_TODO,
REMOVE_TODO
} from '../constants';
export default function Todo(state = initialState, action) {
switch (action.type) {
// Cuando la acción "UPDATE_INPUT_VALUE" es enviada,
// asignamos la clave "inputValue" de Immutable.Map.
case UPDATE_INPUT_VALUE:
return state.set('inputValue', action.payload);
// Cuando la acción "CREATE_TODO" es enviada,
// añadimos el nuevo elemento al final de
// Immutable.List
case CREATE_TODO:
return state.set('todos',
state.get('todos').push(Immutable.Map({
title: action.payload
}))
);
// Cuando la acción "REMOVE_TODO" es enviada,
// borramos el elemento por el indice dado de
// la Immutable.List.
case REMOVE_TODO:
return state.set('todos',
state.get('todos').delete(action.payload));
default:
return state;
}
}
La declaración switch que se usa aquí nos debe parecer familiar - es el mismo patrón que hemos
estado implementando las stores a lo largo de este libro. De hecho, esta función es como una store,
con dos diferencias principales. La primera diferencia es que es una función en lugar de una clase.
Esto significa que en lugar de establecer los valores de las propiedades estado, devolvemos el nuevo
estado. La segunda diferencia es que Redux maneja la mecánica de la escucha para almacenar y
llamar a esta función reductora. Con las clases, tenemos que escribir mucho de este código por
nosotros mismos.
Es importante que estas funciones reductoras no muten el argumento del estado. Esta
es la razón por la que estamos utilizando la biblioteca Immutable.js para
facilitar la transformación del estado existente mediante la creación de nuevos datos.
No es necesario usar Immutable.js para transformar el estado de store de
Redux, pero ayuda con la brevedad del código.
Echemos un vistazo a la función reductora Done:
import Immutable from 'immutable';
import initialState from '../initial-state';
import { CREATE_DONE } from '../constants';
export default function Done(state = initialState, action) {
switch (action.type) {
// Cuando la acción "CREATE_DONE" es enviada,
// insertamos un nuevo objeto al principio
// de la Immutable.List.
case CREATE_DONE:
return state.set('done',
state.get('done')
.insert(0, Immutable.Map(action.payload))
);
// Nada que hacer, devuelve el estado “como es”.
default:
return state;
}
}
Casi hemos terminado con nuestra store Redux. En este punto, tenemos dos funciones reductoras,
cada una en su propio módulo. Necesitamos unirlas usando combineReducers() y
createStore(). Echemos un vistazo a nuestro módulo de store ahora:
import { combineReducers, createStore } from 'redux';

import initialState from './initial-state';


import Todo from './reducers/todo.js';
import Done from './reducers/done.js';

export default createStore(combineReducers({


Todo,
Done
}), initialState);
Como puedes ver, la función combineReducers() crea una nueva función. Esta es la función
reductora principal que mantiene el estado de la aplicación. Así que en lugar del típico dispatcher
Flux que necesita manejar acciones para llevarlas a varias stores, las acciones Redux son enviadas a
esta única store, y nuestras funciones reductoras son llamadas en respuesta.

Acciones Redux
Como sabes, hay una diferencia entre las acciones y las creadoras de acciones. Las acciones son las
payloads que se envían a las distintas stores Flux, mientras que las creadoras de acciones son
responsables de crear las payloads de acción y, a continuación, enviarlas al dispatcher. Con Redux,
las funciones creadoras de acciones son ligeramente diferentes en el sentido de que sólo crean las
payloads de acción, no hablan directamente con el dispatcher.
Veremos cómo se llama a las creadoras de acción en la siguiente sección cuando implementemos
los componentes de la vista. Pero por ahora, así es como se ve nuestro módulo de acciones:
import {
CREATE_TODO,
CREATE_DONE,
REMOVE_TODO,
UPDATE_INPUT_VALUE
} from './constants';
// Crea un nuevo elemento Todo. El "payload" debería
// ser un objeto con una propiedad "title".
export function createTodo(payload) {
return {
type: CREATE_TODO,
payload
};
}
// Crea un nuevo elemento Done. El "payload" debería
// ser un objeto con una propiedad "title".
export function createDone(payload) {
return {
type: CREATE_DONE,
payload
};
}
// Borra un todo y el “payload” del índice dado.
export function removeTodo(payload) {
return {
type: REMOVE_TODO,
payload
};
}
// Actualiza el estado "inputValue" con el valor
// string del "payload" dado.
export function updateInputValue(payload) {
return {
type: UPDATE_INPUT_VALUE,
payload
};
}
Estas funciones simplemente devuelven los datos que va a enviar la store; en realidad no envían los
datos. La excepción es cuando se trata de acciones asícronas. En ese caso, en realidad necesitamos
enviar la acción una vez que se hayan resuelto los valores asíncronos. Vea la documentación oficial
de Redux donde hay muchos ejemplos de acciones asíncronas.

Renderizando componentes y enviando acciones


En este punto, tenemos una store de Redux y funciones de creación de acciones. Todo lo que queda
por hacer es implementar nuestros componentes React y conectarlos a la store. Empezaremos con la
vista TodoList:
import React from 'react';
import { Component } from 'react';
import { connect } from 'react-redux';
import {
updateInputValue,
createTodo,
createDone,
removeTodo
} from '../actions';
class TodoList extends Component {
constructor(...args) {
super(...args);
this.onClick = this.onClick.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
this.onChange = this.onChange.bind(this);
}
render() {
// El estado relevante desde la store "todo"
// que estamos renderizando aquí.
const { todos, inputValue } = this.props;
// Renderiza un input para nuevos todos, y el listado
// de los actuales todos. Cuando el usuario escribe
// y después pulsa enter, el nuevo todo es creado.
// Cuando el usuario cliquea un todo, se mueve al
// array "done".
return (
<div>
<h3>TODO</h3>
<div>
<input
value={inputValue}
placeholder="TODO..."
onKeyUp={this.onKeyUp}
onChange={this.onChange}
autoFocus
/>
</div>
<ul>
{todos.map(({ title }, i) =>
<li key={i}>
<a
href="#"
onClick={this.onClick.bind(null, i)}
>{title}</a>
</li>
)}
</ul>
</div>
);
}
// Un Todo activo fue clicado. La “key” es el
// indice del Todo en la store. Este es
// pasado como payload a la acción
// "createDone()", y seguido a la acción "removeTodo()".
onClick(key) {
const { dispatch, todos } = this.props;
dispatch(createDone(todos[key]));
dispatch(removeTodo(key));
}
// Si el usuario ha introducido algún texto y la
// tecla "enter" es presionada, usamos la
// acción "createTodo()" para crear un nuevo
// elemento usando el texto introducido. Entonces limpiamos
// el input usando la acción
// "updateInputValue()", pasandole un string vacío.
onKeyUp(e) {
const { dispatch } = this.props;
const { value } = e.target;
if (e.which === 13 && value) {
dispatch(createTodo(e.target.value));
dispatch(updateInputValue(''));
}
}
// El value del input cambia – modifica la store.
onChange(e) {
this.props.dispatch(
updateInputValue(e.target.value));
}
}
// Las props que se han pasado a este componente
// desde la store. Sólo necesitamos convertir la
// estructura "Todo" Immutable.js a objeto JS simple.
function mapStateToProps(state) {
return state.Todo.toJS();
}
// Exporta la versión "connected" del
// componente que conecta con la store Redux.
export default connect(mapStateToProps)(TodoList);
Lo más importante de este módulo es que no es el componente clase el que es exportado. En su
lugar, usamos la función connect() del paquete react­redux. Esta función conecta la store
de Redux a esta vista. El estado de la tienda pasa a través de la función mapStateToProps(),
que determina cómo se asignan a React las propiedades de los componentes. En este caso, sólo
tenemos que transformar el Immutable.js en un objeto JavaScript simple.
La desventaja de los manejadores de eventos es que necesitamos enlazar su contexto en el
constructor, porque React no enlaza automáticamente el contexto para los componentes ES2015.
Los handlers necesitan acceder a this.props porque tiene la función dispatch() necesaria
para enviar nuestros datos de acción a la store, así como los datos de la store utilizados para
construir los payloads de acción. Ahora veamos el componente DoneList:
import React, { Component } from 'react';
import { connect } from 'react-redux';
class DoneList extends Component {
render() {
// El array "done" es el único estado que necesitamos
// de la store "done".
const { done } = this.props;
// Quere mostrar estos elementos
// como texto tachado.
constitemStyle = {
textDecoration: 'line-through'
}
// Renderiza el listado de elementos done, con
// el "itemStyle" aplicado a cada elemento.
return (
<div>
<h3>DONE</h3>
<ul>
{done.map(({ title }, i) =>
<li key={i} style={itemStyle}>{title}</li>
)}
</ul>
</div>
);
}
}
// Las props que se pasan a este componente
// desde la store. Sólo necesitamos convertir la
// estructura "Done" Immutable.js a objeto JS sencillo.
function mapStateToProps(state) {
return state.Done.toJS();
}
// Exporta la versión "connected" del
// componente que conecta al store Redux.
export default connect(mapStateToProps)(DoneList);
Como puedes ver, funciona de la misma manera que el componente TodoList. De hecho, estos
componentes no han cambiado mucho en relación con la implementación de Alt de la misma
aplicación. El último paso es conectar los dos componentes con la store de Redux, lo cual se puede
lograr usando el componente Provider:
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import store from './store';
import TodoList from './views/todo-list';
import DoneList from './views/done-list';
// Renderiza los compoenentes "TodoList" y
// "DoneList". El componente "Provider" es
// usado para conectar la store a los componentes.
// Cuando el estado de la store cambia, los hijos
// de "Provider" son re-renderizados.
render(
<Provider store={store}>
<div>
<TodoList/>
<DoneList/>
</div>
</Provider>,
document.getElementById('app')
);

Sumario
En este capítulo, aprendiste sobre el aprovechamiento de las librerías Flux. En particular, echamos
un vistazo a dos de las librerías prevalecientes que pueden ser usadas para implementar
arquitecturas Flux.
Comenzamos el capítulo con una discusión que fue mayormente una recapitulación de los
principios fundamentales de Flux y cómo los implementamos a lo largo de los capítulos anteriores
de este libro. A continuación, cubrimos algunos de los distintos puntos débiles de implementar Flux-
como dispatchers singleton, código de acción repetitiva, y partición de los módulos de store. Estas
son áreas que una biblioteca como Alt.js o Redux podría administrar por nosotros.
A continuación se procedió a implementar una sencilla aplicación de todo utilizando la biblioteca
Alt.js de Flux. La idea detrás de Flux es implementar todos los componentes relevantes mientras
automatizamos las arduas tareas de implementación típicas entre bastidores. Después de esto, nos
centramos en la biblioteca de Redux. Redux está menos preocupado con seguir los patrones de Flux
ferreamente. En cambio, Redux apunta a la simplicidad mientras toma prestadas algunas de las
ideas más importantes de Flux como el flujo de datos unidireccional.
En el próximo capítulo, cubriremos dos aspectos muy importantes de cualquier arquitectura Flux-
las pruebas funcionales y de rendimiento.
13
Pruebas y rendimiento
Queremos que la arquitectura de nuestra aplicación sea lo mejor que pueda ser. Puede sonar tonto
tener que decir esto, pero vale la pena repetirlo de vez en cuando, como recordatorio de que el
trabajo que estamos haciendo con Flux tiene el potencial de sumar o restar éxito a la aplicación. Las
mejores herramientas que tenemos en nuestro arsenal son las pruebas unitarias y pruebas de
rendimiento. Estas dos actividades son iguales de importantes. Ser funcionalmente correcto pero
lento como el infierno es un fracaso. Ser rápido como el infierno y estar plagado de bugs es un
fracaso.
Un factor que contribuye en gran medida a la implementación de pruebas exitosas es concentrarse
en lo relevante que se está haciendo. Vamos a pasar tiempo en este capítulo pensando acerca de lo
importantes que son las pruebas para arquitecturas Flux, tanto desde una perspectiva funcional
como de rendimiento. Esto es especialmente importante pensando en lo nuevo que es Flux para la
comunidad. Nos centraremos en componentes específicos del Flux y diseñaremos algunas pruebas
unitarias para ellos. Entonces pensaremos en la diferencia entre benchmarking a nivel de
planificación de necesidades frente a las pruebas de rendimiento de extremo a extremo.

Hola Jest
Jasmine es una herramienta ampliamente aceptada cuando se trata de escribir de manera efectiva
pruebas unitarias en JavaScript. No hay escasez de herramientas adicionales para Jasmine que
permiten probar casi cualquier cosa y utilizar cualquier herramienta para realizar tus pruebas. Por
ejemplo, es una práctica común usar un ejecutador de tareas como Grunt o Gulp para ejecutar
pruebas, junto con otras varias tareas de construcción asociadas con el proyecto.
Jest es una herramienta de pruebas unitarias, desarrollada por Facebook, que aprovecha las mejores
partes de Jasmine mientras que agrega nuevas capacidades. También es fácil administrar Jest en
nuestros proyectos. Por ejemplo, los proyectos que dependen de Webpack generalmente dependen
de scripts NPM para realizar varias tareas, a diferencia de un ejecutador de tareas. Esto es fácil de
hacer con Jest, como veremos en un momento.
Hay tres aspectos clave de Jest que nos ayudarán a testear nuestras arquitecturas Flux:
• Jest provee un entorno virtualizado JavaScript, incluyendo una interface DOM.
• Jest genera múltiples procesos de trabajo para ejecutar nuestras pruebas, lo que reduce el
tiempo de espera para que las pruebas se completen y acelera el ciclo de vida del desarrollo
en general.
• Jest puede simular módulos JavaScript por nosotros, haciendo más fácil aislar unidades de
código para probar.
Veamos un ejemplo rápido para ver las cosas funcionando. Supongamos que tenemos la siguiente
función que querríamos testear:
// Construye y devuelve un string basado
// en el argumento “name”.
export default function sayHello(name = 'World') {
return `Hello ${name}!`;
}
Esto sería suficientemente sencillo, sólo necesitamos escribir una unidad de testeo que compruebe
la salida esperada. Veamos como se vería en Jest:
// Tells Jest that we want the real "hello"
// module, not the mocked version.
jest.unmock('../hello');
// Importa la función que queremos testear.
import sayHello from '../hello';
// Tu típica suite de testeo Jasmine, casos de test,
// y tests de aserción.
describe('sayHello()', () => {
it('says hello world', () => {
expect(sayHello()).toBe('Hello World!');
});
it('says hello flux', () => {
expect(sayHello('Flux')).toBe('Hello Flux!');
});
});
Si esto se parece mucho a Jasmine, es porque lo es. Jasmine se utiliza realmente de manera
subyacente para realizar todas las pruebas de aserción. Sin embargo, en la parte superior del módulo
de prueba, puedes ver que hay una llamada a la función unmock() de Jest . Esto le dice a Jest que
no queremos una versión simulada de la función sayHello(). Queremos probar la real.
En realidad, hay mucho que hacer para que Jest funcione con las importaciones del
módulo ES2015. Pero en lugar de tratar de explicar eso aquí, yo recomendaría mirar el
código fuente que se envía junto con este libro. Y ahora, volvamos a lo importante.
Creemos un módulo main.js que importe la función sayHello() y la llame:
import sayHello from './hello';
sayHello();
La prueba de unidad de Jest que creamos para la función sayHello() aisló la función
sayHello(). Es decir, no tuvimos que probar ningún otro código para probar esta función. Si
aplicamos esta misma lógica al módulo principal, no deberíamos tener que confiar en el código que
implementa sayHello() . Aquí es donde viene la capacidad de simulación de Jest. Nuestra
última prueba desactivó la función simuladora para el módulo hello, donde sayHello() está
definida. Esta vez, queremos simular la función. Veamos cómo es la prueba principal:
jest.unmock('../main');
// El módulo "main" es dónde está el negocio. La
// función "sayHello()" es una simulación.
import '../main';
import sayHello from '../hello';
describe('main', () => {
// Esperamos que el módulo "main" llame
// a "sayHello()" exactamente una vez. La función
// "sayHello()" que importamos aquí es la misma simulación
// que desde main, podemos verificar que esto es de verdad
// examinando lo que main está haciendo ahora mismo.
it('calls sayHello()', () => {
expect(sayHello.mock.calls.length).toBe(1);
});
});
Esta vez, nos estamos asegurando de que el módulo main.js no sea simulado por Jest. Esto
significa que la función sayHello() que hemos importado es de hecho la versión simulada. Para
verificar que el módulo principal funciona como se espera, tan simple como el módulo es, basta con
verificar que la función sayHello() fue llamada una vez.

Probando a las creadoras de acciones


Ahora que tenemos una idea aproximada de cómo funciona Jest, es el momento de empezar a
probar los distintos componentes de nuestra arquitectura Flux. Empezaremos con las funciones de
creación de acciones, ya que éstas determinan los datos que entran en el sistema y son el punto de
partida del flujo de datos unidireccional. Hay dos tipos de creadoras de acciones que queremos
probar. Primero, tenemos las funciones síncronas básicas, seguidas por las asíncronas. Ambos tipos
de acciones conducen a tipos muy diferentes de pruebas unitarias.

Funciones síncronas
El trabajo de una creadora de acciones es crear los datos payload necesarios y enviarlos a las stores.
Así que para probar esta funcionalidad, queremos la función creadora de acción real y un
componente dispatcher simulado. Recuerda, la idea es aislar el componente como la única unidad
que se está probando; no queremos que ningún efecto secundario del código del dispatcher influya
en el resultado de la prueba. Dicho esto, echemos un vistazo a la creadora de acción:
import dispatcher from '../dispatcher';
export const SYNC = 'SYNC';
// Tu típica función creadora de acción
// síncrona. Envía una acción con
// datos payload.
export function syncFunc(payload) {
dispatcher.dispatch({
type: SYNC,
payload
});
}
Este tipo de función probablemente ya resulte familiar. Queremos que nuestra prueba unitaria para
esta función verifique si el método dispatch() se llama correctamente o no. Echemos un vistazo
a la prueba ahora:
// Queremos testear la implementación real de "syncFunc()".
jest.unmock('../actions/sync-func');
import dispatcher from '../dispatcher';
import { syncFunc } from '../actions/sync-func';
// El método “dispatch()" es simulado por
// Jest. Lo usaremos en el test para validar
// nuestra acción.
const { dispatch } = dispatcher;
describe('syncFunc()', () => {
it('calls dispatch()', () => {
// Llamando "syncFunc()" debería de enviar una
// acción. Podemos verificar esto asegurándonos de
// que "dispatch()" fue llamada.
syncFunc('data');
expect(dispatch.mock.calls.length).toBe(1);
});
it('calls dispatch() with correct payload', () => {
syncFunc('data');
// Tras llamar "syncFunc()", podemos obtener
// información del argumento desde la simulación.
const args = dispatch.mock.calls[1];
const [ action ] = args;
// Asegura que la información correcta fue
// pasada al dispatcher.
expect(action).toBeDefined();
expect(action.type).toBe('SYNC');
expect(action.payload).toBe('data');
});
});
Esto funciona exactamente como esperamos. El primer paso es decirle a Jest que no simule el
módulo sync-func, usando la función unmock(). Jest aún simulará todo incluyendo el dispatcher.
Así que cuando esta prueba llama a syncFunc(), está llamando a la función el dispatcher
simulado a su vez. Cuando lo hace, el simulacro registra información que luego utilizamos en
nuestras pruebas de aserción para asegurarnos de que todo funciona como se esperaba.
Bonito y sencillo, ¿no? Las cosas se ponen un poco más difíciles cuando necesitamos simular la
asincronía pero intentaremos simplificar todo en la siguiente sección.

Funciones asíncronas
Jest hace que sea fácil para nosotros aislar el código que una prueba de unidad dada que debería
probar simulando todas las partes irrelevantes. Algunas cosas se manejan fácilmente con el
generador de simulaciones Jest. Otras necesitan nuestra intervención, como verás en este ejemplo.
Así que comencemos y echemos un vistazo a la función creadora de acción asíncrona que estamos
intentando probar:
import dispatcher from '../dispatcher';
import request from '../request';
export const ASYNC = 'ASYNC';
// Hace una llamada a "request()" (la cual realmente sólo
// un alias para "fetch()") y envía
// la acción "ASYNC" con la respuesta JSON
// como payload de acción.
export function asyncFunc() {
return request('https://httpbin.org/ip')
.then(resp => resp.json())
.then(resp => dispatcher.dispatch({
type: ASYNC,
payload: resp
}));
}
Esta creadora de acción hace una petición a un endpoint público JSON, y luego envía la acción
ASYNC con la respuesta como payload de la acción. Si la función request() que estamos usando
para hacer la petición se parece mucho a la función global fetch(), es porque es esa función. El
módulo de petición simplemente la exporta, de la siguiente manera:
// Exportamos la función global "fetch()"
// de tal manera que Jest tenga oportunidad de simularla.
export default fetch;
Parece inútil, pero no hay nada que hacer. Así es como somos capaces de simular todas las
peticiones en nuestro código fácilmente. Si logramos simular este módulo de petición significa que
nuestro código no intentará llegar al servidor remoto. Para simular este módulo, sólo tenemos que
crear un módulo con el mismo nombre en el directorio __mocks__, junto al directorio
__tests__. Jest simulará, encontrará dicha simulación y la sustituirá por el módulo real cuando
se importa. Echemos un vistazo al código fuente de la simulación de request() ahora:
// Exporta la versión simulada de la función
// "request()" que nuestra creadora de acción usa. En este caso,
// estamos emulando la función "fetch()" y el
// objeto "Response" que devuelve.
export default function request() {
return new Promise((resolve, reject) => {
process.nextTick(() => {
resolve({
json: () => new Promise((resolve, reject) => {
// Aquí es donde ponemos todos nuestros datos extraidos
// simulados. Una función dada debería testear
// las propiedades en las que está interesada,
// ignorando el resto.
resolve({ origin: 'localhost' });
})
});
});
});
}
Si este código se ve un poco asqueroso, no te preocupes, está confinado a este único lugar. Todo lo
que es replicar la interfaz de la función nativa fetch() que este módulo reemplaza (porque en
realidad no queremos traer nada). La parte difícil de esto es que cualquier llamada a request()
en nuestro código va a tener los mismos valores devueltos. Pero esto debería estar bien, asumiendo
que nuestro código puede ignorar propiedades que que no le importa y que podemos mantener los
datos de las pruebas aquí al mínimo.
En este punto, tenemos una capa de red simulada, lo que significa que estamos listos para
implementar la prueba de unidad real ahora. Sigamos adelante y hagámoslo:
jest.unmock('../actions/async-func');
// El "dispatcher" es simulado mientras "asyncFunc()"
// no.
import dispatcher from '../dispatcher';
import { asyncFunc } from '../actions/async-func';
describe('asyncFunc()', () => {
// Para testear código asíncrono que devuelve
// promesas, usamos "pit()" en lugar de "it()".
pit('dispatch', () => {
// Una vez la llamada a "asyncFunc()" ha sido devuelta,
// podemos realizar nuestros test de aserción.
return asyncFunc().then(() => {
// Recoge estadísticas sobre el método
// simulado "dispatch()".
const { calls } = dispatcher.dispatch.mock;
const { type, payload } = calls[0][0];
// Asegura que la función asíncrona
// envía una acción con el payload
// apropiado.
expect(calls.length).toBe(1);
expect(type).toBe('ASYNC');
expect(payload.origin).toBe('localhost');
});
});
});
Hay dos cosas importantes a tener en cuenta acerca de esta prueba. Una, está usando pit().
funciona como reemplazo de it() . Dos, la propia función asyncFunc() devuelve una
promesa. Estos dos aspectos de Jest son los que hacen que la escritura de pruebas unitarias
asíncronas sea tan sencilla. La parte difícil de este ejemplo no es la prueba, es la infraestructura que
necesitamos para simular cosas como las peticiones de red. Gracias a todo lo que Jest hace por
nosotros, nuestro código de prueba de unidad es realmente mucho más pequeño de lo que sería de
otro modo.
Prueba de stores
En la sección anterior, usamos Jest para probar las funciones de creación de acciones. Esto no era
muy diferente de probar cualquier otra función JavaScript, excepto que las creadoras de acciones de
Flux necesitan de alguna manera enviar las acciones que crean las stores. Jest nos ayuda a
conseguirlo simulando automáticamente ciertos componentes, y sin duda nos ayudará a probar los
componentes de nuestra store.
En esta sección, analizaremos la ruta básica de una acción que se envía a una store y la store que
emite un evento de cambio. Luego, pensaremos en el estado inicial de la store y cómo esto puede
llevar a errores que las pruebas unitarias deberían ser capaces de detectar. Hacer que todo esto
funcione va a implicar pensar en implementar código de store comprobable, que es algo en lo que
aún tenemos que pensar en este libro.

Probando los listeners de la store


Los componentes de la store pueden ser difíciles de aislar de otros componentes. Esto a su vez hace
que diseñar pruebas unitarias para store sea difícil. Por ejemplo, una store típicamente se registrará
a sí misma en el dispatcher pasándole una función callback. Esta es la función que cambiará el
estado de la store, dependiendo del payload de acción que se le pase. La razón por la que esto es un
desafío es porque está estrechamente acoplado con el dispatcher.
Idealmente, queremos que el dispatcher sea retirado completamente de la prueba unitaria. Sólo
estamos probando nuestro código de store en la prueba unitaria, así que no queremos que nada de lo
que está pasando al dispatcher interfiera con el resultado. Las probabilidades de que esto ocurra son
escasas, ya que el dispatcher no tiene mucho que hacer. Sin embargo, es mejor ser consistente con
todos nuestros componentes Flux y de alguna manera aislarlos completamente. Hemos visto cómo
puede ayudarnos Jest en la sección anterior. Sólo necesitamos aplicar de alguna manera este
principio a las stores, para desacoplarlos del dispatcher durante las pruebas unitarias.
Este es un caso en el que podríamos necesitar reconsiderar cómo escribimos nuestro código de
store-a veces para que el código sea bueno, necesita ser cambiado levemente de modo que sea
bueno y comprobable. Por ejemplo, la función anónima que normalmente registraríamos con el
dispatcher se convierte en un método de store. Esto permite que la prueba llame al método
directamente, saltándose todo el mecanismo de envío, que es exactamente lo que queremos.
Echemos un vistazo al código de la store ahora:
import { EventEmitter } from '../events';
import dispatcher from '../dispatcher';
import { DO_STUFF } from '../actions/do-stuff';
var state = {};
class MyStore extends EventEmitter {
constructor() {
super();
// Registra un método de esta store como
// manejador, para mejor soporte de la unidad de testeo.
this.id = dispatcher.register(this.onAction.bind(this));
}
// En lugar de realizar la transformación de estado
// en la función que es registrada con el
// dispatcher, sólo determina que método
// de store llamar. Esto enfoca un mejor soporte
// de testeo.
onAction(action) {
switch (action.type) {
case DO_STUFF:
this.doStuff(action.payload);
break;
}
}
// Camiba el "state" de la store, y emite
// un evento “change".
doStuff(payload) {
this.emit('change', (state = payload));
}
}
export default new MyStore();
Como puedes ver, el método onAction() está registrado con el dispatcher, y será llamado cada
vez que se envíe una acción. El método doStuff() rompe el método específico que tiene lugar en
respuesta a la acción DO_STUFF de la directiva onAction(). Esto no es estrictamente necesario,
pero nos proporciona otro objetivo para nuestras pruebas unitarias. Por ejemplo, podríamos haber
dejado la llamada anónima y que nuestras pruebas apunten al método doStuff() directamente.
Sin embargo, si nuestras pruebas llaman a onAction() con el mismo tipo de dato payload que
viene del dispatcher, tenemos mejor cobertura de pruebas de la store.
El lector astuto podría haber notado que esta store está importando EventEmitter desde un
lugar diferente de lo habitual-../events. ¿Tenemos nuestro propio módulo de eventos? Nosotros
sí y es la misma idea que con la función fetch() en la sección anterior. Estamos proporcionando
un módulo propio que Jest puede simular. Esta es una manera fácil para Jest para simular la clase de
EventEmitter. Estábamos tan ocupados pensando en el dispatcher, que olvidamos desacoplar nuestra
store del emisor de eventos para nuestra prueba. Vamos a echar un vistazo al módulo de eventos
para que puedas ver que todavía estamos exponiendo el viejo y bueno EventEmitter que todos
conocemos y amamos:
// En orden a simular la API de Node "EventEmitter",
// necesitamos exponerla a través de uno de nuestros própios módulos.
import { EventEmitter } from 'events';
export { EventEmitter as EventEmitter } ;
Esto significa que los métodos heredados por nuestra store serán simulados por Jest, lo cual es
perfecto porque ahora nuestra store está completamente aislada de otros códigos de componentes y
podemos usar los datos recogidos por el simulador para realizar algunas pruebas de aserción. Vamos
a implementar la prueba unitaria para esta store ahora:
// Queremos testear la store real...
jest.unmock('../stores/my-store');
import myStore from '../stores/my-store';
describe('MyStore', () => {
it('does stuff', () => {
// Llama directamente al método de la store que es
// registrado con el dispatcher, pasándole
// el mismo tipo de datos que el dispatcher
// haría.
myStore.onAction({
type: 'DO_STUFF',
payload: { foo: 'bar' }
});
// Obtiene algo de información de llamada simulada a "emit()"...
const calls = myStore.emit.mock.calls;
const [ args ] = calls;
// Ahora podemos afirmar que la store emite un
// evento "change" y que tiene la información correcta.
expect(calls.length).toBe(1);
expect(args[0]).toBe('change');
expect(args[1].foo).toBe('bar');
});
});
Lo bueno de este enfoque es que se parece mucho a la forma en que los datos fluyen a través de la
store, pero sin depender realmente de otros componentes para ejecutar la prueba. Los datos de test
entran en la store del mismo modo que con un componente de dispatcher real. Del mismo modo,
sabemos que la store está emitiendo los datos de evento correctos midiendo la implementación
simulada. Aquí es donde terminan las responsabilidades de la store, y también las responsabilidades
de la prueba.

Pruebas de condiciones iniciales


Una cosa que aprenderemos poco después de que nuestras stores Flux se vuelvan grandes y
complejas es que se vuelven cada vez más difíciles de probar. Por ejemplo, si el número de acciones
que una store devuelve aumenta, entonces el número de configuraciones de estado que querremos
probar con también aumentará. Para ayudar a acomodar las pruebas unitarias para nuestras stores,
sería útil poder establecer el estado inicial de la store. Echemos un vistazo a una store que nos
permite establecer el estado inicial y responde a un par de acciones:
import { EventEmitter } from '../events';
import dispatcher from '../dispatcher';
import { POWER_ON } from '../actions/power-on';
import { POWER_OFF } from '../actions/power-off';

// El estado inicial de la store...


var state = {
power: 'off',
busy: false
};
class MyStore extends EventEmitter {
// Asigna el estado inicial de la store al argumento
// dado si le ha sido proveído.
constructor(initialState = state) {
super();
state = initialState;
this.id = dispatcher.register(this.onAction.bind(this));
}
// Descifra que acción fue enviada y llama al
// método apropiado.
onAction(action) {
switch (action.type) {
case POWER_ON:
this.powerOn();
break;
case POWER_OFF:
this.powerOff();
break;
}
}
// Cambia el estado power a "on", si el estado power es
// "off" actualmente.
powerOn() {
if (state.power === 'off') {
this.emit('change',
(state = Object.assign({}, state, {
power: 'on'
}))
);
}
}
// Cambis el estado power a "off" si "busy" es falso y
// el estado power actual es "on".
powerOff() {
if (!state.busy && state.power === 'on') {
this.emit('change',
(state = Object.assign({}, state, {
power: 'off'
}))
);
}
}
// Obtiene el estado...
get state() {
return state;
}
}
export default MyStore;
Esta store responde a las acciones POWER_ON y POWER_OFF. Si te fijas en los métodos que
manejan las transformaciones de estado de estas dos acciones, se puede ver que el resultado
depende del estado actual. Por ejemplo, para encender una store se necesita que la store ya esté
apagada. Apagar una store es aún más restrictivo: la store tiene que estar apagada y no puede estar
ocupada. Este tipo de transformaciones de estado necesitan ser probadas usando diferentes estados
iniciales de store, para asegurarse de que el funciona correctamente tanto en casos normales como
los extremos. Ahora echemos un vistazo a la prueba para esta store:
// Queremos testear la store real...
jest.unmock('../stores/my-store');
import MyStore from '../stores/my-store';
describe('MyStore', () => {
// El estado por defecto inicial de la store es
// apagado. Este test asegura que
// el envío de la acción "POWER_ON" cambia el
// estado power de la store.
it('powers on', () => {
let myStore = new MyStore();
myStore.onAction({ type: 'POWER_ON' });
expect(myStore.state.power).toBe('on');
expect(myStore.state.busy).toBe(false);
expect(myStore.emit.mock.calls.length).toBe(1);
});
// Este test cambia el estado inicial de la store
// cuando es instanciado por primera vez. El estado inicial
// ahora es apagado, y hemos marcado también la
// store como ocupada. Este test asegura que la
// lógica de la store funciona como se espera – el estado
// no debería de cambiar, y ningún evento será emitido.
it('does not powers off if busy', () => {
let myStore = new MyStore({
power: 'on',
busy: true
});
myStore.onAction({ type: 'POWER_OFF' });
expect(myStore.state.power).toBe('on');
expect(myStore.state.busy).toBe(true);
expect(myStore.emit.mock.calls.length).toBe(0);
});
// Este test es como el de arriba, sólo que la
// propiedad "busy" es falsa, lo cual significa que deberíamos
// estar capacitados para apagar la store cuando la
// acción "POWER_OFF" es enviada.
it('does not powers off if busy', () => {
let myStore = new MyStore({
power: 'on',
busy: false
});
myStore.onAction({ type: 'POWER_OFF' });
expect(myStore.state.power).toBe('off');
expect(myStore.state.busy).toBe(false);
expect(myStore.emit.mock.calls.length).toBe(1);
});
});
La segunda prueba es quizás la más interesante porque asegura que no se emitan eventos como
resultado de la acción, debido a la forma en que funciona la lógica de transformación de estado de
la store.

Metas de rendimiento
Es hora de cambiar de marcha y pensar en probar el rendimiento de nuestra arquitectura Flux. La
prueba de rendimiento de un componente en particular puede ser difícil por la misma razón por la
que probar la funcionalidad de un componente es difícil: tenemos que aislarlo de otro código. Por
otro lado, a nuestros usuarios no les importa necesariamente el rendimiento de los componentes
individuales, sólo la experiencia general del usuario.
En esta sección, discutiremos lo que estamos tratando de lograr con nuestra arquitectura Flux en
términos de rendimiento. Comenzaremos con el rendimiento percibido por el usuario de la
aplicación, porque éste es el aspecto más consecuente de un bajo rendimiento arquitectura. A
continuación, pensaremos en medir el rendimiento en bruto de nuestros componentes Flux. Por
último, consideraremos las ventajas de establecer requisitos de rendimiento para cuando
desarrollemos nuevos componentes.

Rendimiento percibido por el usuario


Desde el punto de vista de nuestros usuarios, nuestra aplicación responde o se retrasa. Esta
sensación se denomina rendimiento percibido por el usuario, porque el usuario no está realmente
midiendo cuánto tiempo tarda en completarse algo. En general, el rendimiento percibido por el
usuario se mide en umbrales de frustración. Cada vez que tenemos que esperar por algo, la
frustración crece porque no nos sentimos en control de la situación. No podemos hacer nada para
que se acelere, en otras palabras.
Una solución es distraer al usuario. Hay veces en que nuestro código tiene que procesar algo y no
hay forma de evitar el tiempo que toma. Mientras que esto está ocurriendo podemos mantener al
usuario informado sobre el progreso de la tarea. Puede que incluso seamos capaces de mostrar parte
de la salida que ya ha sido procesada, dependiendo del tipo de tarea. La otra respuesta es escribir
código con un buen rendimiento, que es algo en lo que debemos de esforzarnos de todos modos.
El rendimiento percibido por el usuario es de vital importancia para el producto de software que
estamos construyendo porque si se percibe como lento, también se percibe como de mala calidad. A
fin de cuentas, lo que importa es la opinión del usuario-así es como medimos si nuestra arquitectura
Flux se escala o no a un nivel aceptable. La desventaja del rendimiento percibido por el usuario es
que es imposible cuantificar, al menos a un nivel cuantitativo. Aquí es donde necesitamos
herramientas que nos ayuden a medir cómo funcionan nuestros componentes.

Rendimiento medido
Las métricas de rendimiento nos indican específicamente dónde se encuentran los cuellos de botella
de rendimiento en nuestro código. Si sabemos dónde están los problemas de rendimiento, entonces
estamos mejor equipados para abordarlos. Desde la perspectiva de una arquitectura Flux, por
ejemplo, podríamos querer saber si las creadoras de la acción están tardando mucho tiempo en
responder, o si las stores están tardando mucho tiempo en transformar su estado.
Hay dos tipos de pruebas de rendimiento que nos pueden ayudar a mantenernos al tanto de
cualquier problemas de rendimiento durante el desarrollo de nuestra arquitectura Flux. El primer
tipo de prueba es la creación de perfiles, y veremos esto con más detalle en la siguiente sección. El
segundo tipo de pruebas de rendimiento es el benchmarking. Este último tipo de pruebas se hace a
un nivel más bajo y es bueno para comparar diferentes implementaciones.
La única pregunta es: ¿cómo hacemos de la medición del rendimiento un hecho cotidiano?, ¿y qué
podemos hacer con los resultados?

Requisitos de funcionamiento
Dado que disponemos de las herramientas necesarias para realizar pruebas de rendimiento, parece
que es posible definir algunos requisitos en torno al rendimiento. Por ejemplo, si alguien está
implementando una store, ¿podríamos introducir un requisito de funcionamiento que dice que una
store no puede tardar más de x milisegundos en emitir un evento de cambio? El lado positivo es que
podíamos confiar razonablemente en el rendimiento de nuestra arquitectura, hasta el nivel de
componentes. El lado negativo es el complejidad involucrada.
Por una parte, el desarrollo del nuevo código se ralentizaría notablemente, porque no sólo
tendríamos que probar la corrección funcional, también tendríamos una estricta de rendimiento a
mostrar. Esto lleva tiempo, y lo más probable es que la recompensa sea ninguna. Digamos que
acabamos pasando un montón de tiempo mejorando el rendimiento de algún componente que
apenas está fallando en el requerimiento. Esto significaría que estamos volcando nuestro trabajo en
algo que es intangible para el usuario.
Esto no quiere decir que las pruebas de rendimiento no se puedan automatizar o que no se deban
realizar en absoluto. Simplemente tenemos que ser inteligentes sobre dónde invertimos nuestro
tiempo probando el rendimiento de nuestro código Flux. La decisión final sobre el rendimiento la
tiene el usuario, por lo que es difícil establecer requisitos concretos que signifiquen un rendimiento
suficiente, pero es realmente fácil perder el tiempo intentando conseguir un rendimiento óptimo que
nadie notará, y mucho menos los clientes.
Herramientas
Las diferentes herramientas que tenemos a nuestra disposición a través de un navegador web son a
menudo suficientes para resolver cualquier problema de rendimiento en nuestra interfaz. Éstas
incluyen los componentes que forman nuestra arquitectura Flux. En esta sección, repasaremos las
tres herramientas principales encontradas entre las herramientas de desarrollo de los navegadores
que querremos usar para perfilar nuestra arquitectura Flux.
Primero están las funciones de creación de acciones, específicamente las funciones asíncronas.
Entonces vamos a pensar en el consumo de memoria de nuestros componentes Flux. Finalmente,
discutiremos la utilización de la CPU.

Acciones asíncronas
La red siempre va a ser la capa más lenta de la aplicación. Incluso si la llamada a la API que
estamos haciendo es relativamente rápida, pero sigue siendo lenta en comparación con otro código
JavaScript. Si nuestra aplicación no hiciera ninguna solicitud de red, sería muy rápido. Tampoco
sería de mucha utilidad. En general, las aplicaciones JavaScript se basan en endpoints de API
remotos como sus recursos de datos.
Para asegurarnos de que estas llamadas de red no causen problemas de rendimiento, podemos
aprovechar el perfilador de redes de las herramientas de desarrollo de navegadores. Este nos
muestra, en gran detalle, lo que cada solicitud está haciendo, y cuánto tiempo tarda en hacerlo. Por
ejemplo, si el servidor tarda mucho tiempo en responder a una petición, esto se reflejará en la línea
de tiempo de la solicitud.
Utilizando esta herramienta, también podemos ver el número de incidencias que están pendientes en
cualquier momento dado. Por ejemplo, tal vez hay una página en nuestra aplicación que está
acosando al servidor con peticiones y abrumándolo. En ese caso, tenemos que repensar el diseño.
Cada petición que miramos en esta herramienta nos permite profundizar en el código que inició la
solicitud. En las aplicaciones de Flux, esto debería ser siempre una función creadora de acción. Con
esta herramienta, siempre sabemos qué funciones creadoras de acciones son problemáticas desde el
punto de vista de la red y podemos hacer algo al respecto.

Memoria de store
La siguiente herramienta de desarrollo que puede ayudarnos a probar el rendimiento de nuestra
arquitectura Flux es el perfilador de memoria. La memoria es obviamente algo con lo que tenemos
que tener cuidado. Por un lado, tenemos que ser considerados con otras aplicaciones que se ejecutan
en el sistema y evitar acaparar la memoria. Por otro lado, cuando tratamos de tener cuidado con la
memoria, terminamos con frecuentes asignaciones/desasignaciones, desencadenando el colector de
basura. Es difícil poner un número en la cantidad máxima de memoria que un componente debería
usar. La aplicación necesita lo que necesita.
En términos de flujo, estamos más interesados en lo que el perfilador de memoria puede decirnos
sobre nuestras stores. Recuerde, las stores son donde es más probable que nos enfrentemos a
problemas de escalabilidad, porque tendrán que manejar más datos de entrada. Por supuesto,
también queremos mantener el ojo echado en la memoria consumida por nuestros componentes de
vista, pero en última instancia son las stores las que controlan cuánta o cuán poca memoria se
consumirá.
Hay dos maneras en las que el perfilador de memoria puede ayudarnos a entender mejor la memoria
de nuestras stores Flux. Primera, está la línea de tiempo de la memoria. Esta vista muestra cómo se
asigna/desasigna la memoria a lo largo del tiempo. Esto es útil porque nos permite ver cómo se usa
la memoria mientras interactuamos con la aplicación de la misma manera que un usuario lo haría.
Segunda, el perfilador de memoria nos permite tomar una instantánea de las asignaciones de
memoria actual. Así es como determinamos el tipo de datos que se están asignando, y el código que
se está ejecutando. Por ejemplo, con una instantánea, podemos ver qué store está tomando la mayor
parte de la memoria.

Uso de CPU
Como viste en la sección anterior sobre el perfilador de memoria, la recolección de basura frecuente
puede causar problemas con la capacidad de respuesta. Esto es porque el recolector de basura
bloqueará la ejecución de cualquier otro código JavaScript. El perfilador de CPU puede realmente
mostrarnos cuánto tiempo de CPU está llevando el recolector de basura de otro código. Si es
mucho, entonces podemos pensar en una mejor estrategia de memoria.
Una vez más, sin embargo, debemos centrar nuestra atención en los componentes de la store de
nuestra arquitectura Flux al perfilar la CPU. La sencilla razón es que esto nos arrojará un mayor
resultado de como se invierten los recursos de nuestra CPU. Los problemas de escalabilidad a los
que probablemente nos enfrentaremos están centrados en las funciones de transformación de datos
utilizadas para manejar payloads de acción dentro de las stores. A menos que estas funciones sean lo
suficientemente eficientes para manejar los datos que entra en el sistema, la arquitectura no escalará
porque la CPU está siendo sobreutilizada por nuestro código. Y con eso, dirigiremos nuestra
atención al benchmarking de las funciones que es algo crítico para la escalabilidad de nuestros
sistemas.

Herramientas de benchmarking
En un extremo del espectro de pruebas de rendimiento, está el rendimiento percibido por el usuario.
Aquí es donde uno de nuestros clientes se queja de retraso, y seguro que, es fácil para nosotros
replicar el problema. Esto podría ser un problema con los componentes de vista, peticiones de red, o
algo en nuestra store que está causando la subóptima experiencia de usuario. En el otro extremo del
espectro, tenemos el benchmarking sin tratar en el que queremos sincronizaciones precisas para
asegurarnos de que utilizamos la implementación de tecnología más eficiente.
En esta sección, presentaremos brevemente el concepto de benchmarking, y luego mostraremos un
ejemplo que utiliza Benchmark.js para comparar dos implementaciones de transformaciones de
estado.

Código de benchmarking
Cuando comparamos nuestro código, estamos comparando una implementación con otra, o
podemos comparar tres o más implementaciones. La clave es aislar las implementaciones de
cualquier otro componente y asegurarse de que cada uno de ellos tienen la misma entrada y
producen la misma salida. Los puntos de referencia son como las pruebas unitarias en cierto
sentido, porque tenemos una unidad de código que aislamos como una unidad y usamos una
herramienta para medir y probar su rendimiento.
Uno de los retos a la hora de realizar este tipo de micro-benchmarks es la precisión en el tiempo.
Otro desafío es crear un ambiente que no sea interrumpido por otras cosas. Por ejemplo, tratar de
ejecutar un benchmark JavaScript en una página web es probable que se enfrente a interferencia de
otras cosas, como el DOM. Benchmark.js hace el trabajo sucio para obtener la medida más
precisa de nuestro código. Dicho esto, vamos a dar un ejemplo.
A diferencia de las pruebas unitarias, los puntos de referencia no son
necesariamente algo que queramos mantener para siempre. Es simplemente una
carga excesiva, y el valor de los puntos de referencia tiende a disminuir cuando hay
cientos de ellos. Probablemente hay algunas excepciones, en las que queremos
mantener los puntos de referencia en el repositorio con fines ilustrativos. Pero en
términos generales, los puntos de referencia pueden ser descartados con seguridad
una vez que el código ha sido implementado o una vez que el rendimiento del
código existente ha sido mejorado.

Transformaciones de estado
Las transformaciones de estado que ocurren dentro de las stores Flux tienen el potencial de detener
el sistema cuando intentamos ampliarlo. Como sabes, el resto de los componentes de Flux en
nuestra arquitectura se escalan bien. Es el volumen de solicitudes añadido y el volumen de datos
añadido lo que causa problemas. Las funciones de bajo nivel que transforman estos datos deben
funcionar bien. Podemos usar una herramienta como Benchmark.js para construir benchmarks
para el código que trabaja con los datos de la store. Aquí hay un ejemplo:
import { Suite } from 'benchmark';
// La función "setup()" es usada por cada benchmark en
// la suite para crear datos para usarlos en el test.
// Esto se ejecuta antes de que todo sea medido.
function setup() {
// El array "coll" estará disponible en cada
// función benchmark porque este recurso es
// compilado en la función benchmark.
const coll = new Array(10000)
.fill({
first: 'First',
last: 'Last',
disabled: false
});
// Deshabilita algunos elementos...
for (let i = 0; i<coll.length; i += 10) {
coll[i].disabled = true;
}
}
new Suite()
// Añade un benchmark que testea la función
// "filter()" para eliminar elementos deshabilitados y la
// función "map()" para transformar las propiedades de la
// string.
.add('filter() + map()', () => {
const results = coll
.filter(item => !item.disabled)
.map(item => ({
first: item.first.toUpperCase(),
last: item.last.toUpperCase()
}));
}, { setup: setup })
// Añade un benchmark que testea un bucle "for..of"
// para construir el array "results".
.add('for..of', () => {
const results = [];
for (let item of coll) {
if (!item.disabled) {
results.push({
first: item.first.toUpperCase(),
last: item.last.toUpperCase()
});
}
}
}, { setup: setup })
// Añade un benchmark que testea la llamada
// "reduce()" para filtrar los elementos deshabilitados
// y realizar las transformaciones de string.
.add('reduce()', () => {
const results = coll
.reduce((res, item) => !item.disabled ?
res.concat({
first: item.first.toUpperCase(),
last: item.last.toUpperCase()
}) : res);
}, { setup: setup })
// Prepara el registro de las salidas de los manejadores de eventos...
.on('cycle', function(event) {
console.log(String(event.target));
})
.on('start', () => {
console.log('Running...');
})
.on('complete', function() {
const name = this.filter('fastest').map('name');
console.log(`Fastest is "${name}"`);
})
.on('error', function(e) {
console.error(e.target.error);
})
// Ejecuta los benchmarks...
.run({ 'async': true });
// →
// Running...
// filter() x 1,470 ops/sec ±1.00% (86 runs sampled)
// for..of x 1,971 ops/sec ±2.39% (81 runs sampled)
// reduce() x 1,479 ops/sec ±0.89% (87 runs sampled)
// Fastest is "for..of"
Como puedes ver, sólo necesitamos añadir dos o más funciones de referencia a la suite, y
ejecutarlas. El resultado son datos de rendimiento específicos que comparan las distintas
implementaciones. En este caso, estamos filtrando y mapeando una matriz de 10.000 elementos. El
enfoque "for..of" destaca como la mejor apuesta en cuanto a rendimiento.
Lo que es importante acerca del benchmarking es que puede descartar falsos supuestos con bastante
facilidad. Por ejemplo, podríamos asumir que debido a que “for..of” supera las
implementaciones alternativas, es automáticamente la mejor opción. Bueno, dos alternativas no
están tan lejos. Así que si realmente preferimos implementar reduce() , probablemente no haya
riesgo al escalar.
El código que viene con este libro implementa algunos trucos para que este ejemplo
funcione con la sintaxis ES2015 usando Babel. Esta es una idea especialmente
buena si estás transpilando tu código de producción usando Babel, para que tus
puntos de referencia reflejen la realidad. También es útil añadir un script npm
bench a tu package.json para facilitar el acceso.

Sumario
El enfoque de este capítulo ha sido probar nuestras arquitecturas Flux. Existen dos tipos de pruebas
que empleamos para hacer esto: funcionales y de rendimiento. Con unidades funcionales,
verificamos que las unidades de código que componen nuestra arquitectura Flux se comportan
como que se espera. Con las unidades de rendimiento, estamos validando que el código está
funcionando en el nivel esperado.
Introdujimos el marco de pruebas Jest para implementar pruebas unitarias para nuestros creadores
de acción y nuestras stores. Luego discutimos las diversas herramientas del navegador que pueden
ayudarnos a solucionar problemas de rendimiento a un alto nivel. Estos son los tipos de cosas que
impactan la experiencia del usuario de una manera tangible.
Cerramos el capítulo con un vistazo al benchmarking de nuestro código. Esto es algo que tiene
lugar a un nivel bajo y que muy probablemente está relacionado con la funcionalidad de
transformación del estado de nuestras stores. Ahora es el momento de considerar las implicaciones
de Flux en todo el ciclo de vida del desarrollo de software.
14
Flux y el ciclo de vida del desarrollo de software
Flux trata sobre todo de la arquitectura de la información. Esta es la razón por la que Flux es un
conjunto de patrones en lugar de un framework de implementación. Cuando diseñamos
arquitecturas front-end que escalan, la implementación específica importa muy poco, en relación
con el diseño del sistema global. Son cosas como los flujos de datos unidireccionales y las rondas
de actualización sincronas las que tienen un impacto duradero en la escalabilidad del sistema. De
hecho, Flux puede ser lo suficientemente influyente como para cambiar la forma en que
desarrollamos nuestro software.
En este capítulo veremos el ciclo de vida del desarrollo de software a través de la lente de Flux.
Abriremos el capítulo con una discusión sobre las posibilidades abiertas con la implementación de
Flux. Luego compararemos los tipos de actividades de desarrollo que tienen lugar al inicio de un
nuevo proyecto Flux con lo que suceden con la maduración de un proyecto Flux.
También pensaremos en los conceptos que hacen que Flux sea atractivo para empezar, y cómo
extraer estas ideas y aplicarlas a otros sistemas de software. Por último, terminaremos el capítulo
con una mirada a la creación de sistemas de Flux monolíticos versus Flux empaquetando
componentes.

Flux está abierto a la interpretación


Un problema con los frameworks JavaScript es que son sólo una instanciación de un espectro
completo de posibles soluciones. Una solución no es tan universal como cabría esperar. Incluso una
especificación como Flux, que contiene sólo un puñado de patrones, está abierta a la interpretación.
El hecho de que sean sólo patrones hace que sea más fácil para un grupo ir e implementar su
software de una manera, mientras que otro grupo utiliza los mismos patrones para implementar su
software como mejor les parezca.
En esta sección, reiteraremos el hecho de que Flux es sólo un conjunto de patrones a seguir.
Revisaremos la posibilidad de usar una librería Flux, cada una de las cuales tiene un punto de vista
diferente sobre la implementación de los patrones Flux. Entonces consideraremos las ventajas y
desventajas de implementar nuestros propios componentes de Flux.

Opción de implementación 1 - sólo patrones


Flux son sólo patrones que debemos seguir. Puede que ni siquiera los sigamos exactamente. La
eficacia del patrón no es lo que es importante, lo que es importante es que obtengamos el valor
fundamental de Flux en nuestro diseño. Por ejemplo, las acciones describen algo que tiene y llevan
consigo un payload de nuevos datos para entrar en el sistema. Una vez que se han planificado los
nuevos datos, éstos continúan en una dirección hasta que son renderizados. Flux utiliza el concepto
de dispatcher y store. Podríamos llamar a nuestra implementación de Flux cinta transportadora si
quisiéramos. Si el flujo de datos es unidireccional y predecible, entonces hemos alcanzado un
objetivo de Flux.
Asimismo, podemos implementar el dispatcher y los componentes de la store a nuestro gusto. Hay
probablemente ajustes que podríamos hacer a un componente de la store que mejoraría su servicio a
nuestra aplicación. Esto puede ser por razones de rendimiento, o puede ser simples conveniencias
para el desarrollador. Cualquiera de estas cosas está bien introducirlas, siempre y cuando el flujo de
datos se mantenga unidireccional y sincronizado.
Estas ideas de flujo de datos unidireccional y rondas de actualización sincronizadas no son únicas
de Flux. Podríamos trabajar dentro de los límites de otras arquitecturas, tales como MVC, y lograr
los mismos principios. Lo que es único de Flux es que nació por frustración. Los ingenieros de
Facebook decidieron que necesitaban un vehículo para establecer explícitamente cómo hacer que
estos principios de diseño sean correctos.

Opción de implementación 2 - usar una librería Flux


Ciertamente no tenemos que implementar cada componente de Flux nosotros mismos. Hay muchas
opciones cuando se trata de librerías Flux. Lo interesante es que este ecosistema de la biblioteca
Flux refuerza la afirmación de que Flux está abierto a la interpretación. Quizás el mejor ejemplo de
esto es Redux. Esta librería no es una implementación de los conceptos descritos en la
documentación de Flux. En cambio, Redux toma una ruta diferente para implementar los principios
de Flux.
Por ejemplo, no hay ningún dispatcher en Redux, y sólo podemos crear una store, que consta de
funciones reductoras. Lo importante es que todavía tenemos el flujo unidireccional y que las rondas
de actualización estén sincronizadas. Luego está Alt.js, que adopta un enfoque más tradicional
para implementar Flux en el sentido de que tiene la misma abstracción como se describe en la
documentación de Flux. Pero Alt.js también construye sus propias ideas además de estos
conceptos para hacer que la implementación de Flux sea mucho más fácil y más agradable.
¿Es todo o nada cuando decidimos aprovechar una librería Flux? No necesariamente. Este miedo a
todo o nada proviene de frameworks monolíticos que prescriben una cierta manera de hacer las
cosas, y no hay una manera fácil de evitarlas. Con las bibliotecas, la idea es ser capaz de recoger y
elegir los bits que necesitas para componer un comportamiento mayor. Tomar la capa de vista en
una arquitectura Flux - esto es lo más común en componentes de React. Sin embargo, ni Redux ni
Alt.js requieren que usemos React. Redux es lo suficientemente pequeño como para que
podamos usar su componente de store para el estado de nuestra aplicación y Alt.js tiene varios
módulos más pequeños que podemos escoger y elegir, probablemente haya varias que nunca
usaremos.

Crea tu propio Flux


Dado que hay tantos enfoques para implementar un sistema Flux, ¿existe alguna utilidad en
implementar una propia? En otras palabras, ¿estaríamos reinventando la rueda creando nuestros
propios componentes de Flux en lugar de depender de uno de las muchas bibliotecas de Flux que
hay por ahí? No, en absoluto. Hay una fuerte posibilidad de que ninguna de las librerías Flux
satisfagan las necesidades de lo que estamos tratando de lograr. O tal vez hay varias cosas sobre los
componentes de Flux que queremos personalizar, así que que tiene menos sentido depender de una
implementación que vamos a cambiar completamente.
La mayor parte del código en este libro se ha basado en nuestras propias implementaciones de
componentes Flux. Hemos confiado en la implementación de referencia del dispatcher Flux, pero
luego fuimos y pusimos en práctica la nuestra, sin mucha dificultad. Lo positivo de implementar
nuestros propios componentes de Flux es que tenemos la libertad de ajustar los componentes para
satisfacer las necesidades de nuestra aplicación a medida que evoluciona. Esto es más difícil de
hacer cuando dependemos de la implementación de alguien.
Una posibilidad es que usemos una librería como Alt.js como inspiración para crear nuestra
propia implementación. De esta manera, podemos implementar las características geniales de esa
biblioteca mientras las modificamos a nuestro antojo. Por otro lado, podríamos estar mejor sólo
usando una librería Flux tal cual. La mejor apuesta es pensar en este tipo de cosas mientras estás
construyendo el esqueleto de la arquitectura Flux. No dependas de ninguna biblioteca por
adelantado, pero decide desde el principio si vas a usar algo como Redux, así no tienes que tirar
demasiados componentes.

Metodologías de desarrollo
En esta sección, veremos las metodologías de desarrollo que se llevan a cabo en diferentes etapas de
un proyecto Flux. Tenga en cuenta que estas son sólo directrices, ya que las metodologías pueden
variar drásticamente de un equipo a otro. Si dos equipos diferentes están implementando un sistema
Flux, sin duda habrá algunos puntos en común.
Primero pensaremos en lo que sucede durante las fases iniciales de un nuevo proyecto Flux.
Entonces pensaremos en los proyectos de Flux que han tenido la oportunidad de madurar, y en lo
que podría suponer añadir una nueva característica al sistema.

Actividades iniciales de Flux


Muchas metodologías de desarrollo de software desaprueban los grandes diseños iniciales. La razón
es simple: pasamos demasiado tiempo diseñando antes de que cualquier software sea escrito y
probadas. La entrega incremental de piezas de software nos da la oportunidad de validar cualquier
suposición que hayamos podido hacer al escribir el código. La pregunta es, ¿requiere Flux un gran
diseño inicial, o podemos implementar de forma incremental partes de un sistema de Flux?
Como has visto antes en el libro, el primer paso para diseñar una arquitectura Flux es escribiendo
código. Al principio, sólo estamos interesados en producir una arquitectura esqueleto para que
podamos tener una idea del tipo de información que nuestros componentes necesitarán. No
perdemos tiempo implementando componentes de interfaz de usuario inicialmente, porque al
hacerlo probablemente será una perdida de tiempo y una distracción de pensar en otros
componentes Flux que necesitaremos, como stores y acciones.
La pregunta es, ¿puede la construcción de una arquitectura esqueleto encajar en el flujo regular del
desarrollo software sin ser un gran diseño? Creo que sí.
No queremos pasar mucho tiempo en una arquitectura esqueleto, porque eso es sólo una receta para
una ley de trivialidad. Podríamos, sin embargo, establecer objetivos de sprint para la construcción
de piezas de la arquitectura esqueleto y su revisión con un grupo mayor. Algo así como un sprint
demo puede ser el foro ideal para decidir si hemos construido suficiente de la arquitectura esqueleto
y si estamos contentos con ella. Entonces es hora de empezar a construir funcionalidades en serio.
Maduración de una aplicación Flux
Una vez que hayamos superado la fase de arquitectura esqueleto, esperamos tener un producto
sólido con características que nuestros clientes disfrutarán usando. Idealmente, esto significa que
hemos llegado a un punto óptimo con nuestra arquitectura Flux: se adapta bien al escalado, es fácil
de mantener, y somos capaces de mantener a nuestros clientes satisfechos mediante la entrega de
nuevas funcionalidades. En otras palabras, la aplicación está madura. Entonces, ¿cómo llegamos a
este punto, y cómo la mantenemos en marcha?
Consideremos una funcionalidad que se nos han pedido que construyamos. Contamos con un
equipo polivalente de programadores para construirla. ¿Cómo debemos proceder para descomponer
la funcionalidad en tareas de ejecución? Flux hace esto bastante fácil de entender, porque hay un
número limitado de tipos de componentes. Así que si podemos reunir un pequeño equipo para
entregar una funcionalidad, entonces una persona puede centrarse en la aplicación de las vistas, otra
en las stores y acciones, y otro para construir los servicios de datos back-end. He aquí una
ilustración de un equipo y los componentes de Flux que construyen para realizar una funcionalidad
de la aplicación:

Un enfoque alternativo sería tener equipos que se centren en los mismos tipos de componentes. Por
ejemplo, un equipo de store se repartiría entre las funcionalidades, pero cada miembro trabajaría en
un componente de store en cualquier momento dado. Este enfoque es inferior porque un equipo de
programadores de Flux que trabajan en la misma entrega tienen una visión colectiva de cómo la
funcionalidad va a proporcionar el máximo valor al cliente.

Tomar ideas prestadas de Flux


Flux nos obliga a pensar en la arquitectura de información de nuestra aplicación en nuevas e
interesantes vías. Raramente se adopta un nuevo enfoque como este en el vacío. Las ideas tienden a
extenderse a otras partes de la pila tecnológica. Con Flux, son los principios arquitectónicos de la
dirección del flujo de datos y de la información basada en características que se destacan por tener
un impacto positivo. Si estas cosas pueden tener un impacto positivo en el código frontend ¿por qué
no pueden influir en el diseño del sistema en su conjunto?

Flujo de datos unidireccional


El flujo unidireccional de datos a través de una arquitectura Flux es probablemente el aspecto clave
que le permite escalar. Por sí mismo, el flujo de datos unidireccional hace que el código que
escribimos sea fácil de razonar. En algunos lugares, este enfoque puede ser un poco más verboso,
pero es un compromiso consciente que hacemos para facilitar la previsibilidad. Por ejemplo, con las
capacidades de enlace bidireccional de datos que se encuentran en algunos frameworks, podemos
salirnos con la nuestra escribiendo menos código. Esto, sin embargo, es una conveniencia para el
desarrollador que compensa la previsibilidad.
Este es el tipo de lección de Flux que de hecho puede ser aplicable a otras áreas de nuestra pila
tecnológica. Por ejemplo, ¿hay fragmentos de código que son difíciles de razonar porque los datos
que fluyen a través de ellos se mueven en varias direcciones? ¿Podemos cambiar eso?
Puede ser difícil hacer cumplir los flujos de datos unidireccionales en la medida en que lo hace
Flux, pero al menos podemos pensar en los beneficios que esto trae al frontend e intenta aplicar los
mismos principios a otro código. Por ejemplo, tal vez no podemos tener un flujo de datos
unidireccional en su lugar pero podemos adelgazar el componente eliminando los flujos que son
particularmente difíciles de predecir.

El diseño de la información es el rey


Las arquitecturas Flux comienzan con la información con la que el usuario interactúa, y las trabajan
hacia atrás, hacia la API. Este enfoque es diferente de otros frontends donde tienes las entidades de
la API y luego creas modelos frontend, y las vistas (o modelos de vista) calculan las
transformaciones necesarias para crear información relevante para el usuario. El reto de poner la
información primero es que podríamos llegar a algo que no es factible desde la perspectiva de la
API.
Sin embargo, si este es el caso, es probable que tengamos una estructura de equipo disfuncional,
para empezar, porque es fácil aislarse en la burbuja tecnológica propia (back-end, red, front-end,
etc.), pero esto simplemente no funciona en un producto impulsado por funcionalidades. Todos los
miembros contribuyentes necesitan saber qué está pasando en cada capa de la pila.
Si podemos clasificar a los equipos para que cada contribuyente sea plenamente consciente de lo
que está sucediendo en las distintas partes de la base de código, entonces podemos adoptar una
actitud de que el desarrollo de la información es el rey hacia el desarrollo de funcionalidades. Flux
trabaja bien esto, y resulta que esta es la mejor manera de servir a nuestros clientes. Si sabemos qué
información se necesita, podemos averiguar cómo conseguirla.
Por otro lado, estamos sesgados sobre lo que se puede y lo que no se puede hacer porque ya
tenemos una API con la que trabajar. Esto, sin embargo, nunca debería ser el factor determinante de
cuándo y cómo somos capaces de implementar una funcionalidad. Al igual que Flux, debemos
diseñar nuestras abstracciones en torno a la información requerida por la funcionalidad, y no al
revés.

Empaquetando Componentes de Flux


En esta última sección, pensaremos en la composición de grandes aplicaciones de Flux desde el
punto de vista de los paquetes. Primero, argumentaremos a favor de una distribución monolítica de
una aplicación Flux, y el punto en el que este enfoque se vuelve insostenible. Luego hablaremos
sobre los paquetes, y cómo nos ayudan a escalar el esfuerzo de desarrollo de la aplicación Flux.
Finalmente, repasaremos un ejemplo de cómo podría funcionar esto.

Argumentos a favor de Flux monolítico


Cualquiera que haya sido atrapado en el infierno de la dependencias sabe que es un lugar
desagradable. En términos generales, nos enfrentamos a estos problemas confiando demasiado en
paquetes de terceros. Por ejemplo, podríamos usar un par de componentes de un archivo de una
biblioteca gigantesca, o podríamos usar una biblioteca excesivamente simple para algo que
podríamos haber escrito nosotros mismos. En cualquier caso, terminamos con más dependencias
que lo que se justifica por el tamaño y alcance de nuestro proyecto.
Sólo porque estamos implementando una arquitectura Flux para nuestra aplicación, no tenemos que
escalarla por el bien del escalado. En otras palabras, todavía podemos utilizar Flux para
aplicaciones sencillas y reconocer el hecho de que aún no hay necesidad de escalarlas. En este caso,
probablemente es mejor evitar dependencias siempre que sea posible.
La composición de nuestra aplicación Flux simple también puede ser monolítica. Con esto, no me
refiero a poner todo en unos pocos módulos. Una aplicación de Flux monolítica se distribuiría como
un único paquete de MNP. Probablemente podamos hacer esto durante bastante tiempo. Por
ejemplo, podríamos enviar software con éxito durante años sin que esto nunca sea un problema. Sin
embargo, cuando la extensibilidad se convierte en un problema, tenemos que repensar la mejor
manera de componer y empaquetar nuestra aplicación Flux.

Los paquetes permiten escalar


Con el tiempo, las aplicaciones se convierten en víctimas de su propio éxito. Si una aplicación se las
arregla para perdurar por bastante tiempo y ganar bastante atención de clientes, eventualmente
tendrá más funcionalidades de las que es factible manejar. Eso no quiere decir que nuestra
arquitectura Flux no puede manejar muchas funcionalidades, sí puede. Pero mira las cosas de el
punto de vista de los clientes. Probablemente no quieren o necesitan todo lo que otros clientes usan.
Esto requiere que pensemos seriamente en la composición de nuestra arquitectura Flux, porque
puedes apostar a que vamos a necesitar una gestión más fina de las funcionalidades. En otras
palabras, funcionalidades instalables. Pero, ¿hasta qué punto necesitan ser finos estos componentes,
y los paquetes a través de los cuales deban instalarse? Bueno, creo que una funcionalidad de alto
nivel podría ser una buena unidad de medida.
Por ejemplo, típicamente modelamos el estado de una funcionalidad de alto nivel dada de nuestra
aplicación en una sola store. Podemos depender de otras funcionalidades que tienen sus propias
stores, y así sucesivamente. Esto significa que nuestra aplicación debe tener en cuenta que un
componente de funcionalidad dado podría no estar instalado en el sistema. Por ejemplo, si íbamos a
crear un componente Flux que implementara la funcionalidad de gestión de usuarios, nuestra
aplicación que carga estos componentes requeriría esta funcionalidad como si fuera cualquier otro
paquete de terceros.

Componentes Flux instalables


En esta sección, repasaremos una aplicación de ejemplo -aunque sea simple- para ilustrar cómo
podemos instalar componentes principales en nuestra aplicación. Es beneficioso poder extirpar las
partes principales de nuestro núcleo porque esto las desvincula de la aplicación, y hace que sea más
fácil usar el paquete en otro lugar.
Comencemos mirando el módulo principal de la aplicación, que nos ayudará a configurar el
contexto de los otros dos paquetes de MNP que constituyen dos funcionalidades principales:
// Los componentes React que necesitamos...
import React from 'react';
import { render } from 'react-dom';
// Las stores y vistas de nuestros “paquetes de funcionalidades”.
import { Users, ListUsers } from 'my-users';
import { Groups, ListGroups } from 'my-groups';
// Los componentes que son el núcleo de la aplicación...
import dispatcher from './dispatcher';
import AppData from './stores/app';
import App from './views/app';
import { init } from './actions/init';
// Construye las stores Flux, pasándolas en el
// dispatcher como un argumento. Así es como estaremos
// capacitados para obtener paquetes de componentes de terceros que se
// comuniquen con nuestra aplicación y viceversa.
const app = new AppData(dispatcher);
const users = new Users(dispatcher);
const groups = new Groups(dispatcher);
// Re-renderizado de la aplicación cuando el estado
// de la store cambie.
app.on('change', renderApp);
users.on('change', renderApp);
groups.on('change', renderApp);
// Renderizado del componente React "App", y sus
// componentes hijos. El dispatcher es pasado
// a los componentes "ListUsers"
// y "ListGroups" viniendo ellos desde distintos
// paquetes.
function renderApp() {
render(
<App {...app.state}>
<ListUsers
dispatcher={dispatcher}
{...users.state}
/>
<ListGroups
dispatcher={dispatcher}
{...groups.state}
/>
</App>,
document.getElementById('app')
);
}
// Envía la acción "INIT", así la
// store "App" poblará su estado.
init();
Empezaremos por la parte superior, donde importamos stores y vistas de los paquetes my­users y
my­groups. Este es el código de nuestra aplicación, pero ten en cuenta que no estamos utilizando
una ruta de importación relativa. Esto se debe a que se instalan como paquetes NPM. Esto significa
que otra aplicación podría compartir fácilmente estos componentes, y que pueden ser actualizados
independientemente de las aplicaciones que los utilizan. Después de estas importaciones, tenemos el
resto de los componentes de aplicación.
El equipo legal de Apple estará encantado de ver que he nombrado la store AppData en
lugar de AppStore.
A continuación, creamos las instancias de la store. Puedes ver que cada store tiene una referencia
pasada al dispatcher. Así es como nos comunicamos con los componentes de Flux de los que
dependemos para componer una aplicación más grande. Vamos a mirar las stores en breve.
La función renderApp() entonces renderiza el componente principal App de React, y los dos
componentes de nuestros paquetes NPM como hijos. Es esta función la que tenemos registrada en
cada una de las instancias de la store, de forma que cuando cualquiera de estas stores cambie la
interfaz de usuario se vuelve a procesar. Finalmente, se llama a la función creadora de acciones
init(), que rellena la navegación principal.
Este módulo principal es clave para poder componer aplicaciones más grandes en aplicaciones más
pequeñas, a través de paquetes Flux instalables por separado. Los importamos y los configuramos
todos en un lugar. El dispatcher es el principal mecanismo de comunicación; se pasa tanto a stores
como a vistas. No tenemos que tocar más de un archivo para que sea importante y haga uso de las
grandes funcionalidades de la aplicación, que es muy importante para el esfuerzo de desarrollo del
escalado.
Ahora echaremos un vistazo a la store de la aplicación (no a la de Apple) para ver cómo son los
datos de navegación manejados:
import { EventEmitter } from 'events';
import { INIT } from '../actions/init';
// El estado inicial de la store "App" tiene
// algunos textos de cabecera y una colección de
// enlaces de navegación.
const initialState = {
header: [ 'Home' ],
links: [
{ title: 'Users', action: 'LOAD_USERS' },
{ title: 'Groups', action: 'LOAD_GROUPS' }
]
};
// El estado actual está vacío por defecto, significa
// que nada será renderizado.
var state = {
header: [],
links:[]
};
export default class App extends EventEmitter{
constructor(dispatcher) {
super();
this.id = dispatcher.register((action) => {
switch(action.type) {
// Cuando la acción "INIT" es enviada,
// asignamos el estado inicial al estado
// vacio, el cual dispara un re-renderizado.
case INIT:
state = Object.assign({}, initialState);
break;
// Por defecto, vaciamos el estado de la store.
default:
state = Object.assign({}, state, {
header: [],
links: []
});
break;
}
// Siempre emitiremos el evento de cambio.
this.emit('change', state);
});
}
get state() {
return Object.assign({}, state);
}
}
Aquí puedes ver que esta store tiene dos conjuntos de estados: uno es para el estado inicial de la
store y el otro es el estado real que se pasa a los componentes vista para el renderizado. El estado
tiene propiedades vacías por defecto, de modo que las vistas que utilizan esta store no muestran
nada. La acción INIT hará que el estado se pueble desde initialState , y esto resultará en la
actualización de la vista.
Echemos un vistazo a la vista ahora:
import React from 'react';
import dispatcher from '../dispatcher';
// El manejador de clicks "onClick()" enviará
// la acción dada. Este argumento es rebotado cuando
// el link es renderizado. Las acciones enviadas
// desde esta función pueden ser manejadas por otros paquetes
// que están compartiendo el mismo dispatcher.
function onClick(type, e) {
e.preventDefault();
dispatcher.dispatch({ type });
}
// Renderiza los enlaces de navegación principal, y
// cualquier elemento hijo. Nada es renderizado
// si el estado de la store está vacío.
export default ({ header, links, children }) => (
<div>
{header.map(title => <h1 key={title}>{title}</h1>)}
<ul>{
links.map(({ title, action }) =>
<li key={action}>
<a
href="#"
onClick={onClick.bind(null, action)}>{title}
</a>
</li>
)
}</ul>
{children}
</div>
);
Cuando el estado de store está vacío, como es por defecto, todo lo que se renderiza es un div, y un
ul vacío. Esto es suficiente para eliminar completamente la vista de la pantalla. El evento de click
es interesante. Está usando el dispatcher para enviar acciones. La acción proviene de los datos de la
store y, de forma predeterminada, esta aplicación actualmente no hace nada con las acciones
LOAD_USERS o LOAD_GROUPS. Pero los dos paquetes que tenemos importados y configurados
en el módulo principal escuchan estas acciones. Esta es una gran parte de lo que hace que este
enfoque sea escalable -diferentes paquetes NPM Flux pueden enviar o reaccionar a las acciones,
pero esto no significa que realmente sucederá.
Esta es la esencia de nuestra aplicación. Ahora echaremos un vistazo al paquete my­users. El
paquete my­groups es casi idéntico, así que no vamos a listar ese código aquí. Primero tenemos
la store:
import { EventEmitter } from 'events';
import { LOAD_USERS } from '../actions/load-users';
import { LOAD_USER } from '../actions/load-user';
// El estado inicial de la store tiene algunos textos
// de cabecera y una colección de objetos usuario.
const initialState = {
header: [ 'Users' ],
users: [
{ id: 1, name: 'First User' },
{ id: 2, name: 'Second User' },
{ id: 3, name: 'Third User' }
]
};
// El estado de la store que será renderizado por
// las vistas. Inicialmente está vacío así que nada será
// renderizado por las vistas.
var state = {
header: [],
users: []
};
export default class Users extends EventEmitter{
constructor(dispatcher) {
super();
this.id = dispatcher.register((action) => {
switch(action.type) {
// Cuando la acción "LOAD_USERS" es enviada,
// poblamos el estado de la store usando el objeto
// estado inicial. Esto causa que la vista se renderice.
case LOAD_USERS:
state = Object.assign({}, initialState);
break;
// Cuando la acción "LOAD_USER" es enviada,
// modificaremos el texto de cabecera por el usuario
// correspondiente al id de “payload”, usando
// su propiedad "name".
case LOAD_USER:
state = Object.assign({}, state, {
header: [ state.users.find(
x => x.id === action.payload).name ]
});
break;
// Por defecto, queremos vaciar el estado de la store.
default:
state = Object.assign({}, state, {
header: [],
users: []
});
break;
}
// Siempre emitimos el evento de cambio.
this.emit('change', state);
});
}
get state() {
return Object.assign({}, state);
}
}
Hay dos acciones clave que esta store maneja. La primera es LOAD_USERS , que toma el estado
inicial y lo usa para poblar el estado de la store. La acción LOAD_USER cambia el contenido del
estado de cabecera, y esta acción se envía cuando se hace clic en un enlace de usuario. De forma
predeterminada, se borra el estado de store. Ahora echemos un vistazo al componente React que
muestra los datos de la store:
import React from 'react';
import { LOAD_USER } from '../actions/load-user';
// Manejador de evento “click” para los elementos de la lista
// usuarios. El dispatcher es pasado en él como un argumento
// porque este paquete Flux no tiene dispatcher,
// confiando en uno obtenido desde la aplicación.
//
// El “id” del usuario que ha sido clicado es pasado en el
// como un argumento. Entonces la acción “LOAD_USER”
// es enviada.
function onClick(dispatcher, id, e) {
e.preventDefault();
dispatcher.dispatch({
type: LOAD_USER,
payload: id
});
}
// Renderiza el componente usando datos desde el estado
// de la store que le fue pasada como props.
export default ({ header, users, dispatcher }) => (
<div>
{header.map(h => <h1 key={h}>{h}</h1>)}
<ul>{users.map(({ id, name }) =>
<li key={id}>
<a
href="#"
onClick={
onClick.bind(null, dispatcher, id)
}>{name}
</a>
</li>
)}</ul>
</div>
)
La diferencia clave entre esta vista y la vista típica de Flux es que el dispatcher se pasa como un
prop. Luego, a medida que se renderizan los enlaces, la instancia del dispatcher es ligada como
primer argumento a la función handler.
Recomiendo encarecidamente descargar y experimentar con el código de este ejemplo. Los dos
paquetes que se instalan son muy simples, lo suficiente para ilustrar cómo podemos poner en
marcha los mecanismos básicos que nos permiten romper funcionalidades principales de la
aplicación y en sus propios paquetes instalables.

Sumario
Este capítulo examina Flux en el contexto más amplio del ciclo de vida del desarrollo de software.
Puesto que Flux es un conjunto de patrones arquitectónicos que debemos seguir, están abiertos a la
interpretación en cuanto a la implementación. En el comienzo de un proyecto Flux, se hace hincapié
en la entrega iterativa de piezas de una arquitectura esqueleto. Una vez que tengamos una aplicación
madura con varias funcionalidades, el enfoque se centra en la gestión de la complejidad.
Luego discutimos la posibilidad de que otras áreas de nuestra pila de tecnología pudieran querer
tomar ideas prestadas de Flux. Cosas como los flujos de datos unidireccionales significan que hay
menos de efectos secundarios y que el sistema en su conjunto es más predecible. Finalmente,
cerramos el capítulo con una mirada a cómo podríamos componer potencialmente aplicaciones más
grandes con funcionalidades instalables por separado hechas de componentes Flux.
Espero que este libro haya sido una lectura esclarecedora sobre la arquitectura Flux. El objetivo no
era necesariamente para concretar la puesta en práctica ideal de Flux- no pienso que haya tal cosa.
En su lugar, quería impartir el estilo de pensamiento que acompaña a los principios importantes de
Flux. Si te encuentras implementando algo, y empiezas a pensar en flujos de datos unidireccionales
y previsibilidad, entonces podría haber tenido éxito.