Las cuatro piezas: State, Action, Reducer, Store
El vocabulario de TCA es pequeño y fijo: cuatro piezas que se repiten idénticas en cada feature de la app. `State` reúne todo el estado en un único valor; `Action` enumera cada evento que puede ocurrir como un caso de un `enum`; `Reducer` es la función pura que, dada una acción, calcula el estado siguiente y devuelve el trabajo pendiente con el exterior como un `Effect`; y `Store` es el motor en tiempo de ejecución que sostiene el estado, corre el reducer y conecta la vista. Esta lección disecciona cada pieza con su código idiomático, explica el rol exacto de cada una y por qué la separación entre las que son dato —`State` y `Action`—, la que es función —`Reducer`— y el que es runtime —`Store`— es la idea que sostiene toda la arquitectura.
Una feature de TCA se describe siempre con las mismas cuatro piezas, y aprenderlas es aprender el noventa por ciento de la librería, porque el resto son variaciones sobre ellas. State es todo el estado reunido en un valor. Action es la lista de todo lo que puede pasar. Reducer es la transición: dado un estado y una acción, produce el estado siguiente y, si hay trabajo con el mundo, lo describe como un Effect. Y Store es el motor que en tiempo de ejecución sostiene el estado, ejecuta el reducer y conversa con la vista. Lo notable de este reparto es la naturaleza distinta de cada pieza: dos son dato inerte, una es una función pura y una es el runtime que las anima. Diseccionémoslas.
- Definir
Statecomo todo el estado de una feature reunido en un único valor de tipostruct. - Definir
Actioncomo elenumque enumera de forma exhaustiva cada evento posible. - Entender el
Reducercomo la transición pura que muta el estado y devuelve unEffect. - Situar el
Storecomo el motor en tiempo de ejecución que conecta el estado con la vista.
State: todo el estado en un valor
El State es un struct que contiene absolutamente todo lo que la feature necesita recordar entre un instante y el siguiente: los campos de un formulario, si algo está cargando, la lista de resultados, qué pantalla está presentada. No hay estado escondido en la vista ni en variables sueltas; si importa para el comportamiento, vive en el State. Al ser un valor —un struct, no una class— cada instantánea es una fotografía completa e independiente del mundo de la feature en ese momento, y eso es lo que permite compararlas, guardarlas y reproducirlas. Se marca con @ObservableState para que SwiftUI observe con grano fino qué campos lee cada vista y solo redibuje lo que cambió.
@ObservableState
struct State: Equatable {
var nombre = ""
var cargando = false
var resultados: [String] = []
}
Action: cada evento como un caso
El Action es un enum que enumera todo lo que puede ocurrirle a la feature. Y aquí conviene una precisión que separa a TCA del código imperativo: los casos de Action no son órdenes, son hechos. No se llama cargarDatos —un imperativo que dice qué hacer— sino botonRecargarPulsado —un hecho que dice qué pasó—. La vista no manda mutar el estado; se limita a informar de un evento, y es el Reducer quien decide qué hacer con él. Esa distinción parece cosmética pero es la que mantiene el flujo unidireccional: quien emite la acción no sabe ni le importa cómo se procesará.
enum Action {
case aparecer
case botonRecargarPulsado
case datosRecibidos([String])
case fallo(String)
}
Como el enum es cerrado, la lista de acciones es la especificación completa de lo que la feature puede hacer: se lee de arriba abajo y no hay sorpresas fuera de ella. Modelar bien este enum es modelar bien la feature.
Reducer: la transición pura
El Reducer es el corazón. Es una función que recibe el estado —como inout— y una acción, muta el estado según el caso, y devuelve un Effect: la descripción del trabajo con el exterior que hay que hacer, o .none si no hay ninguno. Es el único lugar de toda la feature donde el estado cambia y el único que sabe interpretar cada acción. Fíjate en la pieza que aparece aquí y que no es ninguna de las cuatro protagonistas: el Effect. Cuando el reducer necesita hablar con la red o el disco, no lo hace directamente —eso rompería su pureza—; devuelve un Effect que describe el trabajo, y el runtime lo ejecuta fuera, alimentando su resultado de vuelta como una nueva acción.
@Reducer
struct Buscador {
@ObservableState
struct State: Equatable {
var nombre = ""
var cargando = false
var resultados: [String] = []
}
enum Action {
case botonRecargarPulsado
case datosRecibidos([String])
}
@Dependency(\.api) var api
var body: some ReducerOf<Self> {
Reduce { state, action in
switch action {
case .botonRecargarPulsado:
state.cargando = true
return .run { [nombre = state.nombre] send in
let items = try await api.buscar(nombre)
await send(.datosRecibidos(items))
}
case let .datosRecibidos(items):
state.cargando = false
state.resultados = items
return .none
}
}
}
}
Observa el ciclo completo en miniatura: la acción botonRecargarPulsado muta el estado —cargando = true— y devuelve un Effect con .run; ese efecto corre asíncronamente, y cuando termina envía datosRecibidos, que vuelve a entrar por el mismo switch y muta el estado otra vez. El reducer nunca ejecuta la red; solo la describe.
Store: el motor en tiempo de ejecución
Las tres piezas anteriores son dato y función: no hacen nada por sí solas. El Store es lo que las pone en marcha. Es un objeto vivo —una class, no un valor— que sostiene la instancia actual del State, recibe las acciones que le envía la vista con send, se las pasa al Reducer, guarda el estado resultante y ejecuta los Effect que el reducer devuelve. La vista no toca ninguna de las otras piezas directamente: solo tiene un StoreOf<Buscador>, lee el estado a través de él y le envía acciones. El Store es la frontera entre el mundo puro del reducer y el mundo mutable de la interfaz.
State — dato
Un struct con todo el estado de la feature. Una fotografía completa e inmutable del mundo en un instante. Es valor, no objeto.
Action — dato
Un enum con cada evento posible como un caso. Son hechos, no órdenes: dicen qué pasó, no qué hacer.
Reducer — función
La transición pura (inout State, Action) -> Effect. El único sitio donde el estado cambia. Devuelve el trabajo con el exterior, no lo ejecuta.
Store — runtime
El motor vivo que sostiene el estado, corre el reducer, ejecuta los efectos y conecta con la vista. Lo único que la interfaz conoce.
flowchart TD ACTION[Action] --> STORE[Store] STORE --> REDUCER[Reducer] REDUCER --> STATE[nuevo State] REDUCER --> EFFECT[Effect] STATE --> STORE EFFECT -->|produce mas acciones| ACTION STATE --> VIEW[Vista] style STORE fill:#cba6f7,color:#11111b style REDUCER fill:#f9e2af,color:#11111b style STATE fill:#a6e3a1,color:#11111b style ACTION fill:#89b4fa,color:#11111b style EFFECT fill:#f38ba8,color:#11111b style VIEW fill:#89b4fa,color:#11111b
La forma más productiva de guardar estas cuatro piezas no es memorizar sus nombres, sino su naturaleza, porque de ella se deduce cómo se comporta cada una. State y Action son dato puro: valores inertes que se pueden crear, copiar, comparar, serializar y guardar en un log sin que pase nada, porque no ejecutan ni contienen comportamiento. El Reducer es una función pura: no tiene identidad ni vida propia, es solo una regla que transforma dato en dato y describe efectos, y por eso se puede ejecutar mil veces con la misma entrada y dar mil veces la misma salida. El Store es lo único con vida: es el runtime, el que tiene identidad, el que muta, el que habla con el reloj y la red a través de los efectos, el que existe en el tiempo. Esta taxonomía —dos datos, una función, un runtime— es la clave de por qué TCA es testable de un modo que las arquitecturas tradicionales no son: como todo el comportamiento vive en una función pura que opera sobre valores, un test no necesita montar la interfaz ni esperar a la red; construye un State de entrada, le manda una Action y comprueba el State de salida, todo en memoria, síncrono y determinista. La UI, la red y el tiempo quedan empujados al Store y a los Effect, es decir, a la frontera, donde se pueden sustituir por versiones controladas. Separar lo que es dato de lo que es función y de lo que es runtime no es un lujo de purista: es la maniobra que hace que una feature entera se pueda razonar y probar sin encenderla.
- Elige una feature de tamaño medio y escribe su
Statecomo unstruct: enumera cada campo que debe recordar entre eventos, sin dejar estado en la vista. - Escribe su
Actioncomo unenumy comprueba que cada caso nombra un hecho —qué pasó— y no una orden —qué hacer—. Renombra los que sean imperativos. - Escribe el
Reducercon unswitchexhaustivo. Para cada caso, decide qué campos delStatemuta y si debe devolver unEffecto.none. - Identifica en tu reducer al menos un
Effect: una llamada de red o un temporizador. Confirma que el reducer lo describe con.runen vez de ejecutarlo. - Dibuja el flujo de una interacción completa: qué acción la inicia, qué muta el reducer, qué efecto lanza y qué nueva acción trae de vuelta el resultado.