How To

How JavaScript Generators works with Examples

How JavaScript Generators works with Examples

Today I’ve been reading about Redux-Saga. One of the key concepts there is the use of generator functions (function*). Generators are a JavaScript feature I’d heard of before, but I never really needed them in my day-to-day work—probably because I didn’t fully understand why or when to use them.

Today I finally understood their power, when to use them, and how they can help with common scenarios.

If we read the official documentation about generators on MDN, it says:

The Generator object is returned by a generator function and it conforms to both the iterable protocol and the iterator protocol.

MDN also provides this example:

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
// …

After reading that definition and example, a common way to describe a generator is:

A generator is a function whose execution you can pause and resume, using the yield keyword.

I shared this idea with my friend @Bezael Pérez , and he asked me:

“When do you actually need to pause or resume execution?”

That was a great question, so I decided to read more, investigate, and build a small example to make it clearer for both of us.

The Function vs Function*

In JavaScript, normal functions return a single value (or nothing). When you call them, they run from start to finish and then they’re done.

For example, imagine we have a getQuestion function that stores all the questions we want to ask a user. We pass an index as a parameter, with a default value:

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'

This works perfectly. But if I need the next question, I have to call the function again with a different index:

console.log(getQuestion(2))
'What is your age'

In this example, the caller (the “client”) is responsible for knowing the state, and must pass the correct index every time. In a real app, the client might be a UI component.

It would be nice if we could make this easier for the client and encapsulate that “question flow” logic somewhere else.

The Generator

A generator function uses function* together with yield to return multiple values over time. This makes it easy to turn data into an iterable sequence or a data stream. Every time the generator hits yield, it pauses execution and returns that value.

function* register(){
   yield "Hello";
   name = yield "World";
  return `Hello ${name} from Generator` 
}

If we call register(), we might expect it to run like a normal function, but instead it returns a generator object. That object is responsible for controlling the execution.

const registerFlow = register();

The generator object provides a .next() method, which moves execution to the next yield and returns an object with { value, done }.

Here’s a more complete example:

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());
//prints Hi from generator
 
grettingFlow.next("dany");
grettingFlow.next("paredes");

Each call to .next() returns an object like this:

{ value: 'World', done: false }
{ value: 'Hello', done: false }
{
  value: 'Hello dany paredes from generator',
  done: true
}
  • value is the current yield value (or the final return value).

  • done is false while the generator still has steps to execute.

  • When the generator finishes (return), done becomes true.

Now that we have an intuition for generators, let’s play with a real-world use case.

Build a Form Stepper with Generator

I’m using TypeScript + React inside a Next.js app. This example lives in a Client Component in the Next.js App Router (for example, app/page.tsx).

Generator<Y, R, N>

Where:

  • Y = the type of values yielded by the generator

  • R = the return type when the generator finishes

  • N = the type of values passed into .next(value)

Our generator is registerFlow() looks like:

export function* registerFlow(): Generator<
  string,
  { name: string; age: string; email: string },
  string
> {

this means:

  • The generator will yield strings (the questions).

  • The final return value is an object with name, age and email.

  • Each call to .next(...) will pass a string as the answer.

Here’s the full generator:

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,
  };
}

The generator is responsible for:

  • Sending questions to the UI.

  • Receiving answers from the UI.

  • Returning the final registration data.

Using RegisterFlow with NextJS

In this section, I’ll show how to plug the generator into a Next.js Client Component to build a tiny form stepper like this.

First, we create a ref using useRef hook with the generator:

 const formStepRef = useRef(registerFlow());

Then we set up our component state:

  const formStepRef = useRef(registerFlow());
  const [question, setQuestion] = useState<string | undefined>(undefined);
  const [answer, setAnswer] = useState("");
  const [registrationData, setRegistrationData] = useState<
    Registration | undefined
  >(undefined);

On mount, we move to the first step using useEffect hook:

useEffect(() => {
  const initialStep = formStepRef.current.next();
  if (initialStep.done) {
    setRegistrationData(initialStep.value);
    return;
  }
  setQuestion(initialStep.value);
}, []);

Then we handle step changes using .next and done:

 const changeStep = () => {
    const steps = formStepRef.current.next(answer);
 
    if (steps.done) {
      setRegistrationData(steps.value);
      return;
    }
    setQuestion(steps.value as string);
    setAnswer("");
  };

The final code:

"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>
  );
};

Conclusion

I want to confess that Generators can look a bit strange the first time you see function* and yield, but the mental model is simple:

  • A generator lets you pause and resume a function.

  • Each yield is like a “checkpoint” where you can send a value out and later send a value back in.

  • The .next() calls drive the flow from the outside.

I used a generator to model a multi-step form with a Next.js app, but the same idea applies to more complex flows: wizards, onboarding, async workflows, and of course, for example, the Redux-Saga library was what triggered my interest to learn about them , for example doing a small experiment

Once you’re comfortable with simple patterns like this form stepper, reading Redux-Saga code becomes much easier


Real Software. Real Lessons.

I share the lessons I learned the hard way, so you can either avoid them or be ready when they happen.

No spam ever. Unsubscribe at any time.

Discussion