wandres.dev
EL PATRÓN FLUX · flujo unidireccional

Las piezas de Flux: action, dispatcher, store, view

Flux descompone la gestión de estado en cuatro piezas con roles deliberadamente estrechos: la action es un hecho con nombre expresado como dato plano, el dispatcher es el único centro que reparte cada action a todos los stores, el store guarda el estado y la lógica de un dominio sin exponer setters, y la view observa a los stores y se redibuja. Esta lección diseca cada pieza, muestra su código idiomático, explica por qué cada restricción es intencional, y presenta waitFor como la respuesta de Flux a las dependencias entre stores.

⏱ 17 min

Flux no es una librería con muchas funciones; es un reparto de responsabilidades con cuatro papeles. Lo notable de ese reparto no es lo que cada pieza puede hacer, sino lo que se le prohíbe hacer. La action no tiene lógica, el dispatcher no tiene estado, el store no tiene setters públicos y la view no muta nada directamente. Cada pieza es una restricción disfrazada de componente, y al encajarlas obtienes un sistema donde el estado solo puede cambiar por un camino. Vamos a diseccionar las cuatro, entender el rol exacto de cada una y ver por qué sus límites son su virtud.

🎯 Al terminar esta lección sabrás
  • Definir la action como un hecho con nombre expresado en datos planos, no como una orden.
  • Entender el dispatcher como el único punto por el que pasa todo cambio, y su mecanismo waitFor.
  • Describir el store como dueño del estado y la lógica de un dominio, sin setters públicos.
  • Situar la view como observadora de los stores que traduce interacción en nuevas actions.

La action: un hecho con nombre

Una action es un objeto plano que describe algo que ocurrió. No es una orden imperativa —no dice “cambia el contador”— sino un hecho en pasado —“el usuario pulsó incrementar”—. Esa distinción es semántica pero decisiva: quien crea la action no sabe ni le importa quién reaccionará a ella ni cómo. La action solo declara qué pasó, y deja la interpretación a los stores.

// una action es dato plano: un tipo y una carga util
const accionAnadirTarea = {
  type: "ANADIR_TAREA",
  texto: "estudiar flux",
};

// las "action creators" fabrican y despachan la action
function anadirTarea(texto) {
  dispatcher.dispatch({ type: "ANADIR_TAREA", texto });
}

Que la action sea dato plano, y no una llamada a función, tiene una consecuencia enorme que explotaremos en las próximas lecciones: se puede registrar en un log, serializar, enviar por la red, guardar y reproducir. Un hecho es inspeccionable; una orden imperativa se ejecuta y se pierde. El campo type suele ser una cadena constante que actúa como identificador del hecho.

El dispatcher: el centro que reparte

El dispatcher es la pieza más singular de Flux y la que sus herederos abandonarían. Es un singleton —hay exactamente uno en toda la aplicación— cuyo único trabajo es recibir cada action y reenviarla a todos los stores registrados, en secuencia. No guarda estado ni contiene lógica de negocio; es un repartidor de correo que entrega cada carta a todos los buzones.

import { Dispatcher } from "flux";
export const dispatcher = new Dispatcher();

// cada store se registra y recibe TODAS las actions
const tokenTareas = dispatcher.register((accion) => {
  switch (accion.type) {
    case "ANADIR_TAREA":
      storeTareas.anadir(accion.texto);
      storeTareas.emitirCambio();
      break;
  }
});

Su valor está en centralizar: como toda action pasa por un único punto, ese punto es también el único lugar donde observar todo lo que ocurre en la aplicación. Cuando un store necesita que otro procese la action antes que él —porque depende de su resultado—, el dispatcher ofrece waitFor, que pausa un store hasta que los tokens indicados hayan terminado.

dispatcher.register((accion) => {
  if (accion.type === "ANADIR_TAREA") {
    // no proceses hasta que el store de tareas haya actualizado
    dispatcher.waitFor([tokenTareas]);
    storeResumen.recalcular();
  }
});
ℹ️
waitFor es una señal, no una comodidad

waitFor resuelve las dependencias entre stores declarando un orden explícito, y a la vez delata el punto débil de Flux: cuando tienes muchos stores que dependen unos de otros, acabas tejiendo una maraña de waitFor que reintroduce, por la puerta de atrás, la complejidad de ordenación que Flux prometía eliminar. Redux se fijaría en este detalle y respondería con una decisión radical que veremos en la última lección: un solo store, y por tanto ningún waitFor.

El store: estado y lógica de un dominio

El store guarda el estado de un dominio —las tareas, la sesión, las notificaciones— y toda la lógica que lo transforma. Es la única pieza que contiene datos que sobreviven entre acciones. Su rasgo definitorio, y el más importante de todo Flux, es que no expone setters: nadie puede escribir store.tareas = [] desde fuera. El store solo cambia como reacción a una action recibida a través del dispatcher, y cuando cambia, emite un evento para que las vistas se enteren.

import { EventEmitter } from "events";

class StoreTareas extends EventEmitter {
  #tareas = [];

  anadir(texto) {
    this.#tareas.push({ texto, hecha: false });
  }

  obtenerTodas() {
    return this.#tareas; // solo lectura hacia fuera
  }

  emitirCambio() {
    this.emit("cambio");
  }
  suscribir(cb) {
    this.on("cambio", cb);
  }
}

La ausencia de setters no es un descuido: es la restricción central. Al cerrar la puerta de la escritura directa, el store obliga a que todo cambio entre por la única puerta legítima —una action a través del dispatcher—. Ese es el mecanismo que rompe la red bidireccional de la lección anterior.

La view: observa y se redibuja

La view es la capa de React. Se suscribe a los stores de los que depende, lee su estado y se redibuja cuando reciben un evento de cambio. En el Flux original existía un patrón llamado controller-view: un componente cercano a la raíz que se suscribe a los stores y pasa los datos hacia abajo a componentes puros de presentación.

class ListaTareas extends React.Component {
  state = { tareas: storeTareas.obtenerTodas() };

  componentDidMount() {
    storeTareas.suscribir(() =>
      this.setState({ tareas: storeTareas.obtenerTodas() })
    );
  }

  // al interactuar, la vista NO muta el store: despacha una action
  alEnviar = (texto) => anadirTarea(texto);
}

El punto crucial está en alEnviar: cuando el usuario actúa, la vista no toca el store ni cambia nada directamente. Emite una nueva action y deja que el ciclo la lleve de vuelta al dispatcher. La vista es una función de los stores en una dirección, y una fábrica de actions en la otra —nunca un mutador.

Antes de cerrar, un mapa de las cuatro piezas y su única responsabilidad, para fijarlas juntas y no confundir sus fronteras:

📨

action

Un hecho con nombre, expresado como dato plano. No ejecuta nada; solo declara qué pasó. Por ser dato, se puede registrar, serializar y reproducir.

📮

dispatcher

El único canal. Recibe cada action y la reparte a todos los stores en orden, con waitFor para las dependencias. No guarda estado propio.

🗄️

store

Dueño del estado y la lógica de un dominio. Sin setters: solo cambia reaccionando a una action, y avisa emitiendo un evento de cambio.

🖼️

view

Lee los stores y se redibuja; al interactuar el usuario, fabrica una action nueva. Nunca escribe en el store directamente.

Las cuatro piezas son cuatro prohibiciones

Si intentas memorizar Flux como cuatro cosas que hacen cosas, se te olvidará en una semana. Recuérdalo, en cambio, como cuatro prohibiciones cuidadosamente elegidas, porque ahí está el diseño. La action tiene prohibido contener lógica: es dato inerte, un hecho, y por eso se puede registrar, serializar y reproducir —capacidad que sostiene el time-travel debugging que llegaría después—. El dispatcher tiene prohibido guardar estado: es solo un canal, y por eso existe un único lugar por el que pasa absolutamente todo, que es el mirador desde el que se observa el sistema entero. El store tiene prohibido exponer setters: nadie lo escribe desde fuera, y por eso su estado solo puede cambiar por reacción a una action, cerrando la puerta que en el MVC estaba abierta de par en par. La view tiene prohibido mutar: solo lee stores y fabrica actions, y por eso la interacción del usuario no puede cortocircuitar el ciclo. Cada pieza, por separado, parece una limitación arbitraria; juntas construyen una garantía que ninguna podría dar sola: que el estado de la aplicación solo cambia por un camino conocido y observable. Diseñar así —definiendo un sistema por lo que sus partes no pueden hacer— es una de las técnicas más poderosas y menos enseñadas de la ingeniería de software. Flux no te da libertad; te da una jaula bien pensada, y esa jaula es exactamente lo que hace la aplicación grande razonable de nuevo.

⚔️ Ensambla las cuatro piezas
  1. Define tres actions como objetos planos para un dominio pequeño —un carrito, por ejemplo—: añadir, quitar y vaciar. Comprueba que ninguna contiene lógica, solo type y datos.
  2. Escribe un store con estado privado, sin ningún setter público, que solo cambie dentro de su registro en el dispatcher y emita un evento de cambio.
  3. Registra dos stores en el dispatcher donde el segundo dependa del primero, y usa waitFor para ordenarlos. Anota cómo te sentirías si tuvieras diez stores encadenados así.
  4. Crea una controller-view que se suscriba al store, lea su estado y, al interactuar el usuario, despache una action en vez de mutar nada.
  5. Para cada una de las cuatro piezas, escribe en una frase qué tiene prohibido hacer. Si no puedes nombrar la prohibición, aún no has entendido el rol.