Qué los diferencia
Mismo genoma, fenotipos distintos. La familia unidireccional comparte núcleo puro, pero diverge en los dos lugares donde la pureza se topa con la realidad: los efectos y la composición. Esta lección contrasta las cuatro estrategias de efectos —el middleware de Redux con thunk y saga, el Cmd de Elm, el Effect de TCA y el canal de eventos de MVI— y las ordena bajo dos filosofías: el efecto como valor descrito frente al efecto como orden imperativa. Luego compara cómo cada una ensambla muchas piezas pequeñas en una app grande, desde combineReducers hasta el Scope de TCA. La divergencia no está en el corazón, sino en la frontera.
Si las dos primeras lecciones insistieron en lo que une a la familia, esta se dedica a lo que la separa, y lo hace con una tesis precisa: la divergencia no está en el corazón sino en la frontera. El núcleo puro —estado inmutable, transición pura, flujo unidireccional— es prácticamente idéntico en las cuatro. Donde de verdad se distinguen es en los dos puntos exactos en que la pureza se ve obligada a tratar con el mundo sucio: los efectos, es decir, cómo se hace la red, el tiempo y el azar cuando el reducer tiene prohibido tocarlos; y la composición, es decir, cómo se ensamblan muchas funcionalidades pequeñas en una aplicación grande sin que el conjunto se vuelva ingobernable. Entender estas dos divergencias es entender por qué, siendo la misma familia, eliges una u otra según la plataforma y el problema.
- Ver por qué el efecto es el punto donde la familia se ve forzada a divergir.
- Contrastar las cuatro estrategias de efectos: middleware,
Cmd,Effecty canal de eventos. - Distinguir dos filosofías: el efecto como valor descrito frente al efecto como orden imperativa.
- Comparar la composición:
combineReducers, el anidamiento deElm, elScopedeTCAy los reductores deMVI.
El problema común: el reducer no puede tocar el mundo
Toda la familia comparte una misma prohibición: la transición es pura, así que no puede hacer una petición de red, ni leer el reloj, ni tirar un dado. Pero las aplicaciones reales viven de esas cosas.
De ahí nace la única pregunta que las cuatro arquitecturas se ven obligadas a responder, y la primera donde sus respuestas se separan: si el reducer no puede provocar efectos, ¿dónde ocurren? La pregunta es idéntica en las cuatro; las respuestas, no.
Antes de verlas conviene nombrar el eje que las ordena, porque no son cuatro respuestas dispersas sino dos filosofías con dos representantes cada una. En un extremo, el efecto se describe como un valor —un dato que dice “haz esta petición y avísame con este hecho cuando termine”— y se entrega a un runtime que lo ejecuta.
En el otro extremo, el efecto se ejecuta de forma imperativa en un espacio fuera del reducer, sin describirlo primero. La diferencia parece sutil y lo cambia todo: solo el efecto descrito como valor es inspeccionable antes de correr, cancelable a mitad, y comprobable sin tocar la red de verdad.
Cuatro respuestas a la misma pregunta
Redux deja el efecto fuera del reducer, en el middleware. La opción imperativa clásica es redux-thunk: despachas una función que hace el await y despacha más acciones a mano.
La opción declarativa es redux-saga, que modela los efectos como objetos que un intérprete de generadores ejecuta, acercándose así a la filosofía del efecto como valor sin salir de Redux.
Entre medias viven redux-observable sobre RxJS, el listenerMiddleware de Redux Toolkit y, para el estado de servidor, RTK Query, que trata la petición como un recurso declarado y no como una llamada suelta. Redux es, por diseño, el más agnóstico: te da el hueco del middleware y tú eliges la filosofía.
Elm toma la vía pura sin excepción: update devuelve ( Model, Cmd Msg ), donde Cmd es un valor que describe el efecto y que el runtime de Elm ejecuta, devolviendo el resultado como un Msg. Tu código jamás provoca un efecto; solo lo describe. No hay escotilla.
TCA adopta la misma filosofía que Elm en Swift: el reductor devuelve un Effect<Action>, un valor que el Store ejecuta, y las dependencias del mundo —red, reloj, UUID— se inyectan con @Dependency, lo que las hace intercambiables por versiones controladas en un test.
MVI en Android separa dos flujos: el estado continuo viaja por un StateFlow<UiState>, mientras que los efectos de una sola vez —navegar, mostrar un aviso— se emiten por un canal aparte, un Channel o SharedFlow. El reductor permanece puro y los efectos se emiten de forma explícita; bibliotecas como Orbit formalizan esta separación.
Redux · middleware
El efecto vive fuera del reducer. redux-thunk lo ejecuta de forma imperativa; redux-saga lo modela como objetos declarativos. Máxima libertad, mínima garantía.
Elm · Cmd
update devuelve ( Model, Cmd Msg ). El efecto es un valor que el runtime ejecuta y cuyo resultado vuelve como mensaje. Cero efectos en tu código.
TCA · Effect
El reductor devuelve Effect<Action> y el mundo entra por @Dependency. Efectos como valores más dependencias inyectables: la fórmula más testeable.
MVI · canal de eventos
El estado va por StateFlow; los efectos de una sola vez, por un canal aparte. El reductor sigue puro y el evento no se confunde con el estado.
Dos filosofías del efecto
Reordenadas por filosofía, las cuatro respuestas caen en dos bandos. En el bando del efecto como valor están Elm y TCA: el efecto es un dato que devuelve la transición y que un runtime ejecuta por ti, de modo que tu código describe la intención pero nunca la lleva a cabo.
En el bando del efecto imperativo está el thunk de Redux: no describes nada, simplemente haces el await y despachas. MVI queda a medio camino, con un reductor puro pero efectos que emites tú por un canal, más cerca de la descripción que de la orden cruda.
flowchart TD P[reducer puro no hace IO] --> Q[donde va el efecto] Q --> RX[Redux middleware thunk o saga] Q --> EL[Elm devuelve Cmd como valor] Q --> TC[TCA devuelve Effect como valor] Q --> MV[MVI emite por un canal aparte] EL --> RT[runtime ejecuta y responde con un hecho] TC --> RT style P fill:#89b4fa,color:#11111b style Q fill:#f9e2af,color:#11111b style RT fill:#a6e3a1,color:#11111b
Ver las dos filosofías lado a lado en código deja la diferencia sin ambigüedad. Elm devuelve el efecto como parte del resultado de la transición.
-- Elm: el efecto es un valor que el runtime ejecutara por ti
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
Pedir ->
( { model | cargando = True }, obtenerDatos )
Recibido datos ->
( { model | cargando = False, datos = datos }, Cmd.none )
El thunk de Redux, en cambio, ejecuta el efecto sin describirlo primero.
// Redux thunk: el efecto es una orden imperativa fuera del reducer
const pedirDatos = () => async (dispatch) => {
dispatch({ type: 'datos/pedidos' })
const datos = await fetch('/datos').then((r) => r.json())
dispatch({ type: 'datos/recibidos', payload: datos })
}
La consecuencia práctica llega en el test. La versión de Elm se prueba comparando valores: dado este mensaje, el update debe devolver este modelo y este Cmd, y nada toca la red.
La versión imperativa obliga a interceptar fetch, simular el reloj y esperar promesas; el efecto no se puede inspeccionar porque nunca fue un dato. Esta asimetría reaparecerá en la próxima lección, cuando veamos por qué el testing de TCA es el más envidiado de la familia.
La composición: ensamblar muchas piezas en una
La segunda divergencia es cómo se pasa de una funcionalidad a una aplicación entera. Redux reparte un único árbol en secciones con combineReducers y, en la práctica de 2026, con los slices de Redux Toolkit. La composición es sencilla porque el estado es un objeto plano que se trocea, y cada slice es dueño de su rincón sin saber del resto.
Elm compone anidando: un Model contiene otros modelos, un Msg envuelve otros mensajes, y hay que tejer a mano Cmd.map y Html.map para conectar padre e hijo. Es notoriamente verboso.
Tan verboso que el propio Evan Czaplicki acabó recomendando no modularizar antes de tiempo en su charla “The Life of a File”: en Elm, partir en módulos demasiado pronto cuesta más de lo que ahorra, así que conviene dejar crecer un archivo hasta que la estructura pida a gritos separarse.
TCA hizo de la composición su rasgo distintivo —de ahí la palabra Composable en su nombre—: ofrece Scope, ifLet, forEach y un @ReducerBuilder que ensamblan reductores hijos dentro de padres, además de herramientas de navegación construidas sobre esa misma maquinaria. Donde Elm teje a mano, TCA provee operadores de primera clase.
MVI en Android compone por convención más que por operadores: un reductor y un contenedor por funcionalidad, ensamblados con las herramientas del lenguaje y de Coroutines. Esta divergencia en composición es, junto a los efectos, lo que hace que cada arquitectura brille en un tamaño de problema distinto: Elm en apps de complejidad media, TCA en apps grandes que viven de componer, Redux en cualquier tamaño con la disciplina adecuada.
Todo lo que de verdad distingue a estas cuatro arquitecturas ocurre en la frontera entre el núcleo puro y el mundo sucio, y ahí se juega la madurez de un ingeniero. El principiante ve el thunk de Redux y el Cmd de Elm como dos maneras equivalentes de hacer una petición; el experto ve dos filosofías con consecuencias opuestas. El efecto descrito como valor —la vía de Elm y de TCA— se puede inspeccionar antes de ejecutarlo, cancelar a mitad, y sobre todo sustituir por una versión falsa en un test sin tocar la red real, porque no es una acción sino la descripción de una acción. El efecto imperativo del thunk se ejecuta y punto: para probarlo hay que interceptar fetch, simular el reloj y rezar. No es casualidad que la corriente de fondo de 2026 empuje hacia el efecto como valor incluso en el mundo Redux, con redux-saga, con el listenerMiddleware y con RTK Query, que declara la petición en lugar de dispararla a mano. Cuando diseñes tu propia arquitectura de estado, la pregunta que más te va a pesar dentro de un año no es qué reducer escribes, sino cómo cruzas la frontera del efecto: si lo describes o si lo ejecutas. Describe siempre que puedas; tu yo del futuro, el que tenga que testear y depurar eso a las tres de la madrugada, te lo agradecerá con creces.
- Toma una carga de datos sencilla y escríbela dos veces: una con el efecto ejecutado de forma imperativa y otra con el efecto descrito como valor que un runtime ejecuta.
- Intenta testear ambas versiones sin tocar la red real y razona cuál te obliga a interceptar
fetchy cuál te deja sustituir una dependencia. - Dibuja cómo compondrías tres funcionalidades en
Reduxconslicesy enTCAconScope, y compara cuánta ceremonia exige cada una. - Clasifica los efectos de una app que conozcas en dos cubos, estado continuo y evento de una sola vez, y decide qué estrategia encaja con cada cubo.
- Reescribe un
thunkimperativo como un efecto declarado y observa qué ganas en inspeccionabilidad y qué pierdes en inmediatez. - Sitúa cada una de las cuatro arquitecturas en el eje valor-imperativo y justifica por qué
MVIqueda en el medio.