wandres.dev
EL BODY DEL REDUCER · operadores y orden

Operadores del body: `onChange`, `_printChanges` y los modificadores que envuelven

Junto a los reducers que se listan como hermanos existe una segunda dimensión de composición: los modificadores, métodos encadenables que devuelven un reducer nuevo envolviendo al anterior. Esta lección analiza esa mecánica de cebolla, disecciona `onChange` como la herramienta para mantener invariantes derivadas —y las trampas de reentrada que trae consigo—, presenta `_printChanges` como instrumento de diagnóstico cuyo guion bajo es una advertencia formal de API inestable, repasa `dependency` y `signpost`, y fija la regla de alcance que decide si un modificador afecta a una sola línea del `body` o a todo el conjunto.

⏱ 19 min

Un body compone en dos ejes perpendiculares y conviene no confundirlos nunca. El eje vertical es la lista: reducers hermanos que se ejecutan en secuencia sobre el mismo estado. El eje horizontal es el de los modificadores: métodos encadenados con punto que no modifican nada, sino que construyen un reducer nuevo con el anterior dentro, como capas de una cebolla. .ifLet ya lo hacía; onChange, _printChanges, dependency y signpost hacen lo mismo con otros propósitos. Entender que un modificador es un envoltorio —y no una configuración— aclara de golpe por qué su alcance es exactamente el elemento al que se pega, y por qué el orden en que los encadenas también cambia lo que ves.

🎯 Al terminar esta lección sabrás
  • Explicar el modificador como reducer envolvente y deducir de ahí su regla de alcance dentro del body.
  • Usar onChange para mantener estado derivado y reconocer los riesgos de reentrada y cascada que introduce.
  • Diagnosticar con _printChanges, elegir entre sus modos de impresión y entender qué advierte su guion bajo.
  • Aplicar dependency, transformDependency y signpost a un subárbol concreto y envolver un grupo entero con CombineReducers.

El modificador es un reducer que envuelve a otro

Cuando escribes Reduce { ... }.onChange(of: \.filtro) { ... } no estás configurando el Reduce: estás llamando a un método del protocolo Reducer que devuelve un tipo nuevo —un struct genérico que guarda el reducer original como propiedad— y que a su vez conforma a Reducer. Al llegar una acción, el envoltorio ejecuta primero lo que tiene dentro y después aplica su propia aportación. Es el patrón decorador, pero resuelto en el sistema de tipos y sin una sola asignación en el montón.

De esa mecánica se deriva la regla de alcance que más desconcierta al principio: un modificador afecta solo al elemento al que está pegado, no a los que estén encima o debajo en el body. Si lo que quieres es envolver a todo el conjunto, necesitas un contenedor explícito.

// Alcance parcial: solo el segundo Reduce queda instrumentado.
var body: some ReducerOf<Self> {
  BindingReducer()
  Reduce { state, action in ... }
    ._printChanges()
}

// Alcance total: la cebolla envuelve la composicion entera.
var body: some ReducerOf<Self> {
  CombineReducers {
    BindingReducer()
    Reduce { state, action in ... }
  }
  ._printChanges()
}

CombineReducers no aporta comportamiento: agrupa una lista en un único valor para que tengas algo a lo que pegar el modificador. Es el equivalente al paréntesis en una expresión aritmética, y su necesidad es la mejor prueba de que los modificadores no son ajustes globales sino composición.

flowchart TD
A[accion] --> P[capa printChanges]
P --> O[capa onChange]
O --> R[Reduce base muta el estado]
R --> O2[onChange compara antes y despues]
O2 --> P2[printChanges imprime accion y diff]
P2 --> S[estado final y efectos]

onChange: reaccionar al cambio, no a la acción

onChange responde a una pregunta que un switch sobre acciones no sabe contestar bien: «haga lo que haga el usuario, si este valor del estado acaba cambiando, quiero recalcular aquello». Toma un key path —o una función de proyección— hacia un valor Equatable, ejecuta el reducer que envuelve, compara el valor antes y después, y solo si difieren corre el reducer que le pasas en la closure, que recibe el valor anterior y el nuevo.

var body: some ReducerOf<Self> {
  BindingReducer()
  Reduce { state, action in
    // logica propia
    return .none
  }
  .onChange(of: \.filtro) { anterior, nuevo in
    Reduce { state, _ in
      state.visibles = state.todos.filter { $0.coincide(con: nuevo) }
      return .none
    }
  }
}

La proyección puede ser cualquier función del estado, no solo un key path, de modo que también observas valores compuestos: .onChange(of: { [$0.filtro, $0.orden] }) dispara cuando cambia cualquiera de los dos, y .onChange(of: \.items.count) reacciona al tamaño de una colección sin comparar su contenido, que es bastante más barato.

Su virtud es que centraliza una invariante: da igual qué acción haya movido filtro —un binding, una respuesta de red, la restauración de un estado guardado—, la lista visible se recalcula siempre. Escribir esa misma garantía a mano obligaría a recordar el recálculo en cada rama del switch, y la memoria falla.

⚠️
Tres trampas de onChange que se pagan tarde

Primera: el reducer de la closure corre dentro de la misma pasada, así que sus mutaciones son visibles para los reducers que vengan después en el body; si esos a su vez vuelven a tocar el valor observado, tienes una cascada difícil de leer. Segunda: onChange no distingue el origen del cambio, de modo que también dispara cuando el estado lo movió un hijo o una acción interna, lo que puede sorprender si lo estabas usando como si fuera un evento de interfaz. Tercera: es tentador lanzar efectos de red desde aquí; resiste, porque acabas con peticiones disparadas por causas que no controlas. Para trabajo asíncrono asociado a la intención del usuario, la acción explícita sigue siendo el sitio correcto.

_printChanges: la lupa con guion bajo

_printChanges envuelve un reducer para que imprima, en cada acción, la acción recibida y el diff del estado calculado con swift-custom-dump: solo las líneas que cambiaron, con prefijos de más y menos como en un parche. Es la herramienta de diagnóstico más rentable de TCA, y también la más malinterpretada.

var body: some ReducerOf<Self> {
  Reduce { state, action in ... }
    ._printChanges()                 // accion mas diff completo
    // ._printChanges(.actionLabels) // solo la etiqueta de la accion
}

El guion bajo inicial es una convención formal de Point-Free, no un descuido: marca una API no cubierta por versionado semántico, que puede cambiar de firma o desaparecer en cualquier versión menor. Úsala mientras depuras y retírala antes de fusionar. El modo .actionLabels imprime solo el nombre del caso sin los valores asociados, y es el que quieres cuando el estado es grande, cuando el diff domina la consola o cuando el estado contiene datos personales que no deberían acabar en un registro. La implementación vive detrás de #if DEBUG, así que no penaliza una compilación de release, pero eso no es excusa para dejarla escrita.

🔬

Diagnóstico de orden

Pega _printChanges a un solo elemento del body para ver qué estado le llega y qué estado deja. Es la forma más rápida de comprobar quién corre antes que quién.

🧵

Diagnóstico de efectos

Las acciones que llegan de vuelta desde un Effect aparecen igual que las del usuario. Si ves una acción que no despachaste, alguien la envió desde un efecto.

🚨

Diagnóstico de ruido

Un diff que se repite idéntico en acciones consecutivas suele delatar una invariante recalculada de más, y con ella un redibujo innecesario de la vista.

Otros envoltorios: dependencias y medición

La misma mecánica sirve para dos necesidades muy distintas. dependency sobrescribe un valor del entorno solo para el subárbol envuelto, lo que permite que una parte de la app use un reloj o un generador de identificadores distinto del resto sin tocar nada global; transformDependency hace lo mismo con acceso mutable al valor existente, útil cuando quieres decorar en vez de sustituir. signpost, por su parte, emite marcas de os_signpost alrededor de cada ejecución del reducer, de modo que Instruments te muestra en la línea temporal cuánto tarda cada acción.

var body: some ReducerOf<Self> {
  Reduce { state, action in ... }
    .dependency(\.uuid, .incrementing)
    .signpost()
    .ifLet(\.$destino, action: \.destino) { Destino() }
}

Fíjate en que el encadenamiento también tiene orden: cada llamada envuelve el resultado de la anterior, así que signpost mide un reducer que ya incluye la sustitución de la dependencia, y ifLet gobierna un padre que ya está medido. Cuando dos modificadores observan el estado, encadenarlos al revés cambia lo que cada uno ve; cuando solo lo transforman, el orden es indiferente. Averiguar en cuál de los dos casos estás es parte del trabajo.

Modificador Qué aporta al envuelto Cuándo alcanzarlo
onChange Corre un reducer si un valor Equatable cambió Invariantes derivadas del estado
_printChanges Imprime acción y diferencia de estado Depuración local, nunca en producción
dependency Sustituye un valor del entorno en el subárbol Aislar reloj, identificadores o red de una rama
transformDependency Muta el valor existente en lugar de sustituirlo Decorar una dependencia sin recrearla
signpost Emite marcas para Instruments Medir el coste real por acción
ifLet, ifCaseLet, forEach Gobiernan la cardinalidad del hijo Composición de features, no instrumentación
📝
Escribir el tuyo es más fácil de lo que parece

Ninguno de estos modificadores usa API privada: todos son un struct genérico sobre Base: Reducer más una extensión de protocolo que devuelve ese tipo. Si tu equipo necesita un aspecto que la biblioteca no trae —limitar la frecuencia de una acción, exigir una sesión abierta, contabilizar métricas de producto— la solución no es una excepción a la arquitectura sino un envoltorio más, y compone con los oficiales sin acuerdo previo. Es exactamente lo que construirás en la próxima lección.

Los modificadores son aspectos transversales sin magia

Lo notable de este puñado de operadores no es lo que hacen, sino la categoría de problema que resuelven y el precio que no cobran. Registro, trazado, medición de rendimiento, sustitución de dependencias, mantenimiento de invariantes derivadas: son exactamente los cross-cutting concerns que dieron origen a la programación orientada a aspectos hace veinte años, y que en el mundo orientado a objetos siempre se pagaron con maquinaria oscura —proxies dinámicos, intercepción en tiempo de ejecución, tejido de bytecode, anotaciones que reescriben clases—. En TCA se resuelven con un método que devuelve un struct genérico. No hay reflexión, no hay indirección dinámica, no hay contenedor que tenga que descubrirte en tiempo de arranque: hay un valor dentro de otro valor, y el compilador ve la cadena entera y puede especializarla e inlinearla. Esa es la ventaja escondida de haber convertido el reducer en un valor de primera clase en la lección primera de este nivel: al ser un valor, admite ser envuelto, y al admitir ser envuelto, cualquier preocupación transversal se expresa como un envoltorio más en lugar de como una excepción a la arquitectura. La consecuencia práctica es que puedes escribir tus propios aspectos —auditoría, control de acceso, límites de frecuencia— con las mismas herramientas con las que Point-Free escribió los suyos, y que van a componer con los existentes sin ninguna coordinación previa. Lo que en otras arquitecturas exige un framework aparte, aquí es una extensión de protocolo de quince líneas; y ese es justamente el terreno de la próxima lección.

⚔️ Instrumenta una feature y mide el alcance
  1. Pega _printChanges() a un único Reduce de un body con tres elementos y despacha una acción. Anota qué cambios de estado no aparecen en la consola y explica por qué.
  2. Envuelve ahora todo el body con CombineReducers y repite. Compara ambas salidas.
  3. Cambia a _printChanges(.actionLabels) en una feature cuyo estado tenga un array grande y valora la diferencia de legibilidad.
  4. Introduce un onChange sobre un campo que otro reducer del body también modifique más abajo. Provoca la cascada, obsérvala en la consola y reescríbela para que solo un reducer sea dueño de ese campo.
  5. Encadena dependency(\.uuid, .incrementing) y signpost() en los dos órdenes posibles, escribe qué esperas en cada caso y comprueba si acertaste.