DEV Community

Cover image for How to use Generators in JavaScript
Accreditly
Accreditly

Posted on • Edited on • Originally published at accreditly.io

How to use Generators in JavaScript

JavaScript, a language that has been consistently evolving, introduced a powerful feature in its ES6 (ECMAScript 2015) iteration: Generators. While they might seem daunting at first, generators are invaluable tools for handling asynchronous operations and creating custom iterable sequences. Let's unwrap the mystique behind JavaScript generators.

What Are Generators?

Generators are special functions in JavaScript that allow you to yield (or produce) multiple values on a per-request basis. They pause their execution when yielding a value and can resume from where they left off. This "pausing" capability makes generators versatile for many scenarios, particularly asynchronous tasks.

Basic Syntax of Generators

Generators are defined similarly to regular functions but with an asterisk (*). The yield keyword is used to produce a sequence of values.

function* myGenerator() {
  yield 'first value';
  yield 'second value';
  yield 'third value';
}
Enter fullscreen mode Exit fullscreen mode

Using a Generator

To use a generator, you must first call it, which returns a generator object:

const gen = myGenerator();
Enter fullscreen mode Exit fullscreen mode

This object follows the iterator protocol and has a next() method:

console.log(gen.next()); // { value: 'first value', done: false }
console.log(gen.next()); // { value: 'second value', done: false }
console.log(gen.next()); // { value: 'third value', done: false }
console.log(gen.next()); // { value: undefined, done: true }
Enter fullscreen mode Exit fullscreen mode

Benefits of Generators

  1. Unlike traditional functions that might build and return a huge array, generators produce values on the fly. This means you're not storing large data structures in memory.

  2. Together with Promises, generators offer a smoother way to handle asynchronous operations. This synergy gave birth to async/await, which is essentially syntactic sugar over generators and promises.

  3. Beyond producing a sequence of values, generators can be used to define custom iteration behaviors.

Yielding Other Generators

Generators can yield other generators, making them composable:

function* generatorA() {
  yield 'A1';
  yield 'A2';
}

function* generatorB() {
  yield* generatorA();
  yield 'B1';
}

const genB = generatorB();
console.log(genB.next()); // { value: 'A1', done: false }
Enter fullscreen mode Exit fullscreen mode

Generators and Error Handling

You can handle errors in generators with try-catch blocks. If an error is thrown inside a generator, it will set the done property of the generator to true.

function* errorGenerator() {
  try {
    yield 'all good';
    throw new Error('Problem occurred');
  } catch (err) {
    yield err.message;
  }
}
Enter fullscreen mode Exit fullscreen mode

Real-world Use Cases

  1. Fetching chunks of data lazily, such as paginated API results or reading large files in segments.

  2. Generating infinite sequences, like an endless series of unique IDs.

  3. Pausing and resuming functions, allowing for more complex flow control.

Generators offer an alternative and often cleaner approach to handling asynchronous operations and generating sequences in JavaScript. While they've been somewhat overshadowed by the rise of async/await, understanding generators gives a deeper insight into the language's capabilities. With generators in your JS toolkit, you're better equipped to tackle a wider range of programming challenges.

Example: Lazy Loading of Data

Imagine you have an application that needs to load large sets of data from a server, like a list of products in an e-commerce site. Instead of loading all data at once, which can be inefficient and slow, you can use a generator to lazily load the data as needed.

Scenario:

  • You have an API endpoint that returns a list of products.
  • The API supports pagination, allowing you to fetch a limited number of products per request.
  • You want to display these products in batches on the user interface, loading more as the user scrolls.

JavaScript Generator Implementation:

function* dataFetcher(apiUrl, pageSize) {
  let offset = 0;
  let hasMoreData = true;

  while (hasMoreData) {
    const url = `${apiUrl}?limit=${pageSize}&offset=${offset}`;
    yield fetch(url)
      .then(response => response.json())
      .then(data => {
        if (data.length < pageSize) {
          hasMoreData = false;
        }
        offset += data.length;
        return data;
      });
  }
}

// Using the generator
const pageSize = 10;
const apiUrl = 'https://api.example.com/products';
const loader = dataFetcher(apiUrl, pageSize);

function loadMore() {
  const nextBatch = loader.next();
  nextBatch.value.then(products => {
    // Update UI with new batch of products
  });
}

// Initial load
loadMore();

// Subsequent loads, e.g., triggered by scrolling
window.addEventListener('scroll', () => {
  // Load more data when the user scrolls down
  loadMore();
});
Enter fullscreen mode Exit fullscreen mode

Example: Multi-Step Form Navigation

In a web application, you might have a multi-step form where the user needs to complete several steps in sequence. Using a generator, you can create a smooth and controlled navigation flow through these steps.

Scenario:

  • The application has a form divided into multiple steps (e.g., personal details, address information, payment details).
  • The user should be able to move forward and backward between steps.
  • The application should keep track of the current step and display it accordingly.

JavaScript Generator Implementation:

function* formWizard(steps) {
  let currentStep = 0;

  while (true) {
    const action = yield steps[currentStep];

    if (action === 'next' && currentStep < steps.length - 1) {
      currentStep++;
    } else if (action === 'prev' && currentStep > 0) {
      currentStep--;
    }
  }
}

// Define the steps of the form
const steps = ['Step 1: Personal Details', 'Step 2: Address Information', 'Step 3: Payment Details'];

// Create an instance of the form wizard
const wizard = formWizard(steps);

// Function to move to the next step
function nextStep() {
  wizard.next('next').value.then(currentStep => {
    // Update UI to show the current step
  });
}

// Function to move to the previous step
function prevStep() {
  wizard.next('prev').value.then(currentStep => {
    // Update UI to show the current step
  });
}

// Initial step
nextStep();
Enter fullscreen mode Exit fullscreen mode

Top comments (7)

Collapse
 
miggu profile image
miggu

A real useful/clever application of this would have made this article stand out for its uniqueness. Thank you anyway. :)

Collapse
 
accreditly profile image
Accreditly

Thanks for the feedback. I have added a couple of real-world examples, one of which is taken from an application we've actually built in the past. Hopefully this helps illustrate the practicalities a little better

Collapse
 
fininhors profile image
Francisco Junior

Thanks for the article.
However, once again I haven't seen a practical use.
Show an example in a real application.

Collapse
 
accreditly profile image
Accreditly

Hi. Thanks for the feedback. I have added a couple of real world examples (one taken from a live production application), in addition to the real world use-case list.

Collapse
 
mroeling profile image
Mark Roeling • Edited

in Brave console
Image description

note that done: true is the one after the last value!

Collapse
 
mroeling profile image
Mark Roeling

same in case of error:
Image description

Collapse
 
dani_el-dev profile image
Daniel Oyola

Thanks for the article, it has valuable information that, at least to me, was unknown. I'll give it a try in my next project.