wandres.dev
REDUX-SAGA A FONDO · efectos con generadores

Los cuatro verbos: take, put, call y select

Con el mecanismo ya entendido, esta lección instala el vocabulario mínimo con el que se escribe casi cualquier saga y explica por qué ese vocabulario cambia la forma de pensar el asincronismo. call y put cubren el hacer y el anunciar; select abre una ventana de lectura sobre el store en un instante concreto, con el riesgo de lectura rancia que eso implica; y take es el verbo que invierte la relación con el evento, pasando del modelo push del callback al modelo pull de la espera explícita. De ahí sale el rendimiento cognitivo real de la librería: una conversación entre eventos que en callbacks era una máquina de banderas se escribe como un guion que se lee de arriba abajo, y hasta takeEvery se revela como azúcar de un bucle con take y fork.

⏱ 18 min

Casi todo lo que escribirás con sagas cabe en cuatro verbos. call hace, put anuncia, select mira y take espera. Los tres primeros son traducciones directas de cosas que ya sabías hacer con un thunk, con la diferencia de que ahora son descripciones en vez de actos; el cuarto no tiene equivalente cómodo en ningún otro sitio, y es el que justifica de verdad la librería. take da la vuelta a la relación entre tu código y los eventos: en lugar de registrar una función para que alguien la llame cuando algo pase, tu código se planta en una línea concreta y se queda ahí hasta que eso pase. Ese giro de empuje a tirón convierte una coreografía de banderas en un guion, y aprender a leerlo de arriba abajo es la destreza central de este nivel.

🎯 Al terminar esta lección sabrás
  • Distinguir el papel exacto de call, put y select y sus formas menos obvias de invocación.
  • Entender el riesgo de la lectura rancia con select y por qué el estado leído es una foto, no una suscripción.
  • Ver take como inversión del modelo de eventos: de empuje con callbacks a tirón con espera explícita.
  • Leer y escribir una saga como una historia lineal, y reconocer takeEvery como azúcar de un bucle con take y fork.

Hacer y anunciar: call y put

call describe una invocación bloqueante. Bloqueante significa aquí que la saga se suspende hasta que el intérprete resuelve la llamada, ya sea una función síncrona, una que devuelve una promesa o incluso otro generador, que el middleware ejecutará como subsaga hasta agotarlo. Sus formas menos conocidas importan más de lo que parece: call([objeto, objeto.metodo], arg) preserva el this del método, apply(objeto, objeto.metodo, [arg]) es la misma idea con otra sintaxis, y call(otraSaga, arg) compone sagas sin ceremonia, delegando y esperando su terminación. Que todas devuelvan un objeto plano es lo que permite que la composición sea barata: componer sagas es componer descripciones.

put describe un despacho. Es el único canal por el que una saga afecta al estado, y esa exclusividad es deliberada: la saga no muta nada, propone acciones y el reducer decide qué significan. Conviene saber que put es no bloqueante por defecto —la saga sigue sin esperar a que se asienten los suscriptores— y que existe putResolve para el caso en que el reducer o un middleware intermedio devuelva una promesa que sí quieras esperar. Como el orden de los put es observable en el test, el guion de anuncios de una saga se convierte en su especificación de comportamiento.

import { call, put, select, take } from "redux-saga/effects";
import { api } from "./api";

function* guardarPerfil() {
  // select: una foto del estado en este instante exacto
  const borrador: { nombre: string } = yield select((s: any) => s.perfil.borrador);
  yield put({ type: "perfil/guardando" });
  try {
    // call: se suspende aqui hasta que el interprete resuelve la promesa
    const guardado: { id: string } = yield call(api.guardarPerfil, borrador);
    yield put({ type: "perfil/guardado", payload: guardado });
  } catch (e) {
    yield put({ type: "perfil/error", payload: String(e) });
  }
}

select: una foto, no una suscripción

select describe una lectura del estado aplicando un selector, y devuelve el valor vigente en el momento exacto en que el intérprete atiende ese efecto. La palabra clave es momento: no hay suscripción, no hay reactividad, no hay actualización posterior. Lo que obtienes es una foto, y en cuanto la saga vuelve a suspenderse en el siguiente call o take, esa foto puede quedar obsoleta porque el mundo siguió despachando acciones mientras tú esperabas.

De ahí sale el error más silencioso y más caro de las sagas largas: leer el estado al principio, esperar un segundo en una petición y decidir después con datos que ya no son ciertos. La regla práctica es leer tarde y leer cerca —hacer el select justo antes de la decisión que depende de él, no al abrir la saga—, y cuando la corrección dependa de que nada haya cambiado en medio, comprobarlo explícitamente en vez de suponerlo. Es la misma clase de problema que la lectura no atómica en concurrencia clásica, y merece el mismo respeto: entre dos yield de la misma saga puede haber pasado cualquier cosa.

💡
Los selectores del Nivel 5 son los mismos que usas aquí

select acepta cualquier función de estado a valor, así que los selectores memoizados con createSelector encajan sin adaptación: yield select(seleccionarPedidosVisibles). Reutilizarlos, en vez de escribir lambdas ad hoc dentro de las sagas, mantiene una sola definición de cada derivación y evita que la saga conozca la forma interna del store. La frontera de lectura no cambia porque el lector sea un generador en vez de un componente: si un selector es el contrato de lectura de un slice, también lo es para el efecto.

take: de empujar a tirar

Aquí está el giro. El modelo habitual de eventos es de empuje: registras una función y el sistema la llama cuando ocurre algo. Ese modelo fragmenta cualquier proceso de varios pasos en callbacks inconexos, y como cada uno empieza sin memoria del anterior, el estado del proceso hay que guardarlo fuera, en banderas, contadores y referencias. take invierte la dirección: la saga pide el próximo evento y se suspende hasta que llega. El proceso deja de estar troceado porque el contexto vive en el marco del generador, exactamente igual que las variables locales de una función normal.

La consecuencia es que una conversación se escribe en línea recta. Abrir un diálogo, esperar la confirmación, actuar en consecuencia y volver a empezar deja de ser un grafo de callbacks para ser un párrafo. take admite además varias formas de patrón: un tipo de acción, un array de tipos, un comodín para cualquier acción o un predicado arbitrario sobre la acción, lo que permite esperar no ya un tipo sino una condición.

import { take, call, put, fork } from "redux-saga/effects";
import { api } from "./api";

// Una conversacion completa, leida de arriba abajo.
function* flujoDeCompra() {
  while (true) {
    yield take("carrito/pagar");                       // espera aqui
    yield put({ type: "dialogo/abrir" });
    const r: { type: string } = yield take(["dialogo/aceptar", "dialogo/cancelar"]);
    if (r.type === "dialogo/cancelar") continue;       // vuelve al principio
    yield call(api.cobrar);
    yield put({ type: "carrito/pagado" });
  }
}

// takeEvery no es magia: es este bucle con fork no bloqueante.
function* takeEveryCasero(patron: string, worker: (a: any) => Generator) {
  while (true) {
    const accion = yield take(patron);
    yield fork(worker, accion);
  }
}
sequenceDiagram
participant U as usuario
participant S as saga flujoDeCompra
participant R as store
U->>R: dispatch carrito pagar
R-->>S: take se reanuda
S->>R: put dialogo abrir
U->>R: dispatch dialogo aceptar
R-->>S: take se reanuda
S->>S: call api cobrar
S->>R: put carrito pagado

Ese takeEveryCasero de seis líneas es más didáctico que cualquier explicación: los ayudantes de alto nivel que verás en la próxima lección no son primitivas del sistema, sino combinaciones triviales de take y de los efectos de concurrencia. Saberlo cambia tu relación con la librería, porque cuando ninguno de los ayudantes encaje con tu flujo podrás escribir el tuyo en vez de deformar el problema para que quepa en takeEvery.

Componer: el guion dentro del guion

Un guion largo se parte en escenas, y las sagas se componen de dos maneras que parecen equivalentes y no lo son. La primera es yield call(otraSaga, arg): el intérprete recibe una descripción, reconoce que la función es un generador y lo ejecuta como subtarea hasta agotarlo, devolviendo su valor de retorno. La segunda es yield* otraSaga(arg), la delegación nativa de generadores del lenguaje, que no crea ningún efecto: los yield de la subsaga salen directamente al intérprete como si estuviesen escritos en el cuerpo del padre.

La diferencia importa en tres sitios. En el test, call(otraSaga, arg) es un único paso comparable por igualdad, mientras que yield* obliga al test del padre a recorrer también todos los pasos internos de la hija. En la cancelación, call crea una subtarea con identidad propia, y yield* no crea nada que se pueda cancelar aparte. Y en el tipado, yield* es la única forma que TypeScript infiere de verdad, que es exactamente el mecanismo sobre el que se apoya typed-redux-saga. La regla que se deduce es útil: usa call cuando la subsaga sea una unidad conceptual que quieres poder aislar, y yield* cuando solo estés extrayendo un fragmento por higiene de lectura.

import { call, put, take } from "redux-saga/effects";

// Fragmento reutilizable: se inlinea en el guion del padre.
function* pedirConfirmacion(mensaje: string) {
  yield put({ type: "dialogo/abrir", payload: mensaje });
  const r: { type: string } = yield take(["dialogo/aceptar", "dialogo/cancelar"]);
  return r.type === "dialogo/aceptar";
}

function* borrarElemento(id: string) {
  const confirmado: boolean = yield* pedirConfirmacion("Seguro?"); // delegacion: sin efecto intermedio
  if (!confirmado) return;
  yield call(purgar, id); // unidad aislable: un solo paso en el test del padre
  yield put({ type: "lista/borrado", payload: id });
}
📞

call

Describe una invocación bloqueante: función, promesa u otra saga. Admite contexto con array o con apply.

📤

put

Describe un despacho. Único canal de la saga hacia el estado; no bloqueante salvo que uses putResolve.

🔍

select

Describe una lectura del estado en un instante. Es una foto: entre dos yield puede haber quedado rancia.

🎧

take

Describe la espera de una acción. Invierte el modelo de eventos y convierte una coreografía en un guion.

📝
El watcher que no reacciona: sagas que arrancan solas

No toda saga necesita esperar una acción para existir. Una saga de arranque puede hacer su call inicial en la primera línea —cargar configuración, abrir un socket, hidratar una sesión— y solo después entrar en su bucle de take. Que el punto de entrada sea código en vez de un registro declarativo permite mezclar sin fricción lo que ocurre una vez al principio con lo que ocurre cada vez que llega un evento, algo que en un sistema puramente reactivo obliga a inventar una acción artificial de inicio.

take devuelve el tiempo al programa; el callback lo había expulsado

El vocabulario de sagas parece una lista de utilidades y en realidad esconde una tesis sobre dónde debe vivir el tiempo en un programa. En el modelo de empuje —callbacks, oyentes, reducers reaccionando a acciones sueltas— el tiempo está fuera: cada manejador es un instante sin pasado ni futuro, y para expresar que algo va después de otra cosa hay que codificar el paso del tiempo como datos, en banderas, fases y estados intermedios que el programador inventa y mantiene a mano. Ese trabajo de contabilidad es donde nacen la mayoría de los bugs de flujo, porque la secuencia real del proceso no está escrita en ningún sitio: hay que reconstruirla leyendo qué manejador enciende qué bandera. take propone lo contrario. Al permitir que una función se detenga en mitad de su cuerpo a esperar un evento, devuelve al programa la herramienta que siempre tuvo para expresar el tiempo, que es el orden de las líneas. El proceso vuelve a ser un texto que se lee de arriba abajo, el estado intermedio vuelve a ser variables locales y el bucle vuelve a ser un bucle; nada de eso hay que reificarlo en el store porque vive en el marco suspendido del generador. Esa es la razón profunda de que select sea una foto y no una suscripción, de que takeEvery sea derivable en seis líneas y de que la cancelación de la próxima lección resulte casi gratuita: todo son consecuencias de haber recuperado la linealidad. Y también explica el límite de la técnica: si tu efecto no tiene tiempo interno —si es un recado con principio y fin inmediato— no hay linealidad que recuperar, y toda esta maquinaria solo añade ceremonia a algo que ya era una línea.

⚔️ Escribe una conversación en línea recta
  1. Escribe una saga con select, put, call y put que guarde un formulario, y ordena los efectos para que la lectura ocurra lo más tarde posible.
  2. Provoca a propósito una lectura rancia: haz select al principio, espera con un call lento y despacha entre medias una acción que cambie ese dato. Observa la decisión incorrecta.
  3. Corrige el caso anterior moviendo el select después del call y comprueba que ahora decide con datos vigentes.
  4. Escribe un bucle con while y take que modele un diálogo de confirmación con dos salidas, y comprueba que el estado del proceso no necesita ninguna bandera en el store.
  5. Sustituye el patrón de take por un predicado sobre la acción en vez de un tipo, y espera una condición en lugar de un nombre.
  6. Implementa tu propio takeEvery con take y fork, úsalo en lugar del oficial y verifica que se comporta igual.
  7. Reescribe la misma conversación con oyentes y banderas en el store, y compara cuántos sitios hay que leer para reconstruir la secuencia en cada versión.