Cómo funcionan los generadores en JavaScript con ejemplos

Hoy estuve leyendo sobre Redux-Saga. Uno de sus conceptos clave es el uso de funciones generadoras (function*). Los generadores son una característica de JavaScript de la que ya había oído hablar, pero nunca los había necesitado en mi trabajo diario, probablemente porque no entendía del todo por qué o cuándo usarlos.
Hoy por fin entendí su utilidad, cuándo conviene utilizarlos y cómo pueden ayudarnos en escenarios habituales.
Si leemos la documentación oficial sobre generadores en MDN, dice:
El objeto
Generatores retornado por una función generadora y cumple tanto con el protocolo iterable como con el protocolo iterador.
MDN también proporciona este ejemplo:
function* infinite() {
let index = 0;
while (true) {
yield index++;
}
}
const generator = infinite(); // "Generator { }"
console.log(generator.next().value); // 0
console.log(generator.next().value); // 1
console.log(generator.next().value); // 2
// …Después de leer esa definición y el ejemplo, una forma común de describir un generador es:
Un generador es una función cuya ejecución puedes pausar y reanudar, utilizando la palabra clave yield.
Compartí esta idea con mi amigo @Bezael Pérez y me preguntó:
«¿Cuándo necesitas realmente pausar o reanudar una ejecución?»
Fue una excelente pregunta, así que decidí leer un poco más, investigar y construir un pequeño ejemplo para que nos quedara claro a los dos.
Función normal vs Function*
En JavaScript, las funciones normales retornan un único valor (o nada). Cuando las llamas, se ejecutan de principio a fin y terminan.
Por ejemplo, imagina que tenemos una función getQuestion que almacena todas las preguntas que queremos hacerle a un usuario. Pasamos un índice como parámetro, con un valor por defecto:
function getQuestion(question = 0) {
const questions = ['What is your name', 'What is your email', 'What is your age'];
return questions[question];
}
console.log(getQuestion());
// Output:
// 'What is your name'Esto funciona a la perfección. Pero si necesito la siguiente pregunta, tengo que llamar a la función otra vez con un índice distinto:
console.log(getQuestion(2))
'What is your age'En este ejemplo, quien llama a la función (el «cliente») es responsable de conocer el estado, y debe pasar el índice correcto cada vez. En una aplicación real, el cliente suele ser un componente de interfaz de usuario.
Sería genial si pudiéramos facilitarle la vida al cliente y encapsular esa lógica del «flujo de preguntas» en otro lugar.
El Generator
Una función generadora utiliza function* junto con yield para retornar múltiples valores a lo largo del tiempo. Esto facilita convertir datos en una secuencia iterable o en un flujo de información. Cada vez que el generador llega a un yield, pausa su ejecución y retorna ese valor.
function* register(){
yield "Hello";
name = yield "World";
return `Hello ${name} from Generator`
}Si llamamos a register(), podríamos esperar que se ejecute como una función normal, pero en su lugar retorna un objeto generador. Ese objeto es el encargado de controlar la ejecución.
const registerFlow = register();El objeto generador ofrece un método .next(), que avanza la ejecución hasta el siguiente yield y retorna un objeto con { value, done }.
Aquí tienes un ejemplo más completo:
function* gretting() {
yield "Hi from generator";
const name = yield "World";
const lastname = yield "Hello";
return `Hello ${name} ${lastname} from generator`
}
const grettingFlow = gretting();
console.log(grettingFlow.next());
// Imprime Hi from generator
grettingFlow.next("dany");
grettingFlow.next("paredes");Cada llamada a .next() retorna un objeto con esta estructura:
{ value: 'World', done: false }
{ value: 'Hello', done: false }
{
value: 'Hello dany paredes from generator',
done: true
}-
value es el valor actual emitido por yield (o el valor final de retorno).
-
done es false mientras el generador todavía tenga pasos pendientes por ejecutar.
-
Cuando el generador termina (return), done pasa a ser true.
Ahora que entendemos cómo funcionan los generadores, veamos un caso de uso del mundo real.
Crear un formulario por pasos con un Generator
Voy a usar TypeScript + React dentro de una aplicación Next.js. Este ejemplo vive en un Client Component en el App Router de Next.js (por ejemplo, en app/page.tsx).
Generator<Y, R, N>Donde:
-
Y = el tipo de los valores emitidos (yield) por el generador
-
R = el tipo de retorno cuando el generador finaliza
-
N = el tipo de los valores pasados hacia .next(value)
Nuestro generador registerFlow() luce de la siguiente manera:
export function* registerFlow(): Generator<
string,
{ name: string; age: string; email: string },
string
> {Esto significa lo siguiente:
-
El generador emitirá strings (las preguntas).
-
El valor final de retorno es un objeto con name, age y email.
-
Cada llamada a .next(...) recibirá un string como respuesta.
Aquí está el generador completo:
export function* registerFlow(): Generator<
string,
{ name: string; age: string; email: string },
string
> {
const name = yield "What is your name?";
const age = yield "What is your age ?";
const email = yield "What is your email?";
return {
name,
age,
email,
};
}El generador se encarga de:
-
Enviar las preguntas a la interfaz.
-
Recibir las respuestas desde la interfaz.
-
Retornar los datos finales del registro.
Cómo usar RegisterFlow con Next.js
En esta sección, te muestro cómo conectar el generador a un Client Component de Next.js para construir un pequeño formulario por pasos como este:

Primero, creamos una referencia usando el hook useRef con el generador:
const formStepRef = useRef(registerFlow());Luego configuramos el estado de nuestro componente:
const formStepRef = useRef(registerFlow());
const [question, setQuestion] = useState<string | undefined>(undefined);
const [answer, setAnswer] = useState("");
const [registrationData, setRegistrationData] = useState<
Registration | undefined
>(undefined);Al montarse el componente, avanzamos al primer paso con el hook useEffect:
useEffect(() => {
const initialStep = formStepRef.current.next();
if (initialStep.done) {
setRegistrationData(initialStep.value);
return;
}
setQuestion(initialStep.value);
}, []);Después manejamos los cambios de paso utilizando .next y done:
const changeStep = () => {
const steps = formStepRef.current.next(answer);
if (steps.done) {
setRegistrationData(steps.value);
return;
}
setQuestion(steps.value as string);
setAnswer("");
};El código completo queda así:
"use client";
import { useEffect, useRef, useState } from "react";
import { registerFlow } from "./registerflow";
import { clsx } from "clsx";
export type Registration = {
name: string;
age: string;
email: string;
};
const Page = () => {
const formStepRef = useRef(registerFlow());
const [question, setQuestion] = useState<string | undefined>(undefined);
const [answer, setAnswer] = useState("");
const [registrationData, setRegistrationData] = useState<
Registration | undefined
>(undefined);
useEffect(() => {
const initialStep = formStepRef.current.next();
if (initialStep.done) {
setRegistrationData(initialStep.value);
return;
}
setQuestion(initialStep.value);
}, []);
const changeStep = () => {
const steps = formStepRef.current.next(answer);
if (steps.done) {
setRegistrationData(steps.value);
return;
}
setQuestion(steps.value as string);
setAnswer("");
};
return (
<div className="flex flex-col items-center justify-center min-h-screen bg-gray-100">
<div className="bg-white p-8 rounded-xl shadow-md flex flex-col gap-6 w-full max-w-md">
<h1 className="text-3xl font-bold text-gray-800 text-center mb-2">
Registration Page
</h1>
<h3 className="text-lg text-gray-600 text-center min-h-[2em]">
{registrationData ? `Thanks ${registrationData.name}!` : question}
</h3>
{!registrationData && (
<input
className="bg-gray-50 border border-gray-300 text-gray-900 text-sm rounded focus:outline-none focus:ring-2 focus:ring-blue-400 px-4 py-2 w-full transition"
value={answer}
onChange={(e) => setAnswer(e.target.value)}
placeholder="Enter your answer"
/>
)}
<button
onClick={() => changeStep()}
className={clsx(
`mt-4 w-full py-2 px-4 bg-blue-600 text-white text-lg font-semibold rounded hover:bg-blue-700 transition disabled:opacity-50`,
{ invisible: registrationData }
)}
>
{registrationData ? "Finish" : "Next"}
</button>
</div>
</div>
);
};Conclusión
Debo confesar que los generadores pueden verse algo extraños la primera vez que te encuentras con function* y yield, pero el modelo mental es simple:
-
Un generador te permite pausar y reanudar una función.
-
Cada yield funciona como un punto de control donde puedes emitir un valor hacia afuera y luego recibir un valor de regreso.
-
Las llamadas a .next() controlan el flujo desde el exterior.
En este ejemplo usé un generador para modelar un formulario de varios pasos en Next.js, pero la misma idea aplica a flujos más complejos: asistentes de configuración (wizards), procesos de onboarding, flujos asíncronos y, por supuesto, librerías como Redux-Saga, que fue la que despertó mi interés por aprender sobre ellos al realizar un pequeño experimento.
Cuando te familiarizas con patrones simples como este stepper de formulario, entender el código de Redux-Saga se vuelve mucho más sencillo.
- Código fuente: https://github.com/danywalls/wizard-step
Foto por Sergei Gussev en Unsplash
Modern frontend development — Angular, React, TypeScript, accessibility, and performance.
Artículos Relacionados
Compartir este artículo
Si esta guía te resultó útil, compártela con tu equipo o con otros desarrolladores.
Software real. Lecciones reales.
Comparto las lecciones que aprendí por las malas, para que puedas evitarlas o estar preparado cuando ocurran.
Únete a 13,800+ desarrolladores y lectores.
Cero spam. Cancela tu suscripción en cualquier momento.