Master JavaScript generators: 5 inspiring practical use cases

Last updated on December 07, 2023
Master JavaScript generators: 5 inspiring practical use cases

You will be an expert on JavaScript generators by the time you’re done reading this.

JavaScript generators are way more than just a fancy feature and we are going to discover many powerful use cases for them, including creating engaging animations, and streaming videos over the internet.

If you've never heard of them you may be missing out.

Generators are simply functions that you can pause and resume whenever you want -- they don't execute continuously.

The asterisk * marks the function as a generator, and yield generates values on demand from a .next() call, until the generator is done.

function* myGenerator() {
  yield 'Hello';
  yield 'World';
}

const iterator = myGenerator();

console.log(iterator.next()); // {value: 'Hello', done: false}
console.log(iterator.next()); // {value: 'World', done: false}
console.log(iterator.next()); // {value: undefined, done: true}

It's just like how a physical generator produces electricity not all at once, but as time goes on.

Instead of calling next() you can use the for..of loop, which is great for when the generator generates a lot of data.

function* countTo100() {
  for (let i = 0; i < 100; i++) {
    yield i;
  }
}

for (const item of countTo100()) {
  console.log(item);
}
// 1, 2, ... 100

Lazy evaluation

"Calculate only when necessary."

Much unlike regular functions in JavaScript that execute entirely and return the result.

Let's say you want a sequence of numbers, but you're not sure how many. Here's how a generator helps:

function* numberGenerator() {
  let number = 1;
  // infinite loop won't cause freeze in generator
  // -- execution pauses after `yield`
  while (true) {
    yield number++;
  }
}

const numbers = numberGenerator();

console.log(numbers.next().value); // 1
console.log(numbers.next().value); // 2
// you can continue this as long as you need

With generators, you get the next number only when you ask for it.

Better memory utilization

Generators don't hold all the results in memory, they generate them on the fly.

Imagine you need a sequence of a million numbers. With a regular function, you'd need to store all these numbers in an array, using up significant memory.

A generator is much more efficient:

function* bigNumberGenerator() {
  let number = 1;
  while (number <= 100000000) {
    yield number++;
  }
}

const bigNumbers = bigNumberGenerator();

const chunk = 10;
for (let i = 0; i < chunk; i++) {
  const value = bigNumbers.next().value;
  // Use next 10 values...
}

Handling asynchronous tasks

Did you know that Babel transpiles async/await to generators for JavaScript versions that don't support it natively?

Babel tranforms this:

this.it('is a test', async function () {
    const name = await 'coding beauty'
    const num = await new Promise(resolve => resolve(10));
    console.log(`Name: ${name}, Num: ${num}`);
});

To this:

function _asyncToGenerator(fn) {
  return function () {
    var gen = fn.apply(this, arguments);
    return new Promise(function (resolve, reject) {
      function step(key, arg) {
        try {
          var info = gen[key](arg);
          var value = info.value;
        } catch (error) {
          reject(error);
          return;
        }
        if (info.done) {
          resolve(value);
        } else {
          return Promise.resolve(value).then(
            function (value) {
              return step('next', value);
            },
            function (err) {
              return step('throw', err);
            }
          );
        }
      }

      return step('next');
    });
  };
}

myFunc(
  'generator async/await example',
  _asyncToGenerator(function* () {
    const name = yield 'coding beauty'; // yield, not await
    const num = yield new Promise((resolve) => resolve(10));
    console.log(`Name: ${name}, Num: ${num}`);
  })
);

Typing animations

Typing animations grab the attention of users and make your website more visually appealing.

They add personality and character to a website by mimicking the typing behavior of a human to create a more human-like experience and establish a unique brand identity.

So with all these benefits you're feeling excited about infusing your webpages with these energetic visual effects.

Here would be a decent way to go about it, using recursion and setTimeout():

function typeAnimation(text, index = 0) {
  if (index < text.length) {
    document.body.innerHTML += text.charAt(index);
    index++;
    setTimeout(typeAnimation, 500);
  }
}

But, it's cases like this where generators shine:

function* typeAnimation(text) {
  for(let char of text) {
    yield char;
  }
}

Since we can generate values whenever we want, we can do this in time intervals using setInterval().

const generatorInstance = typeGenerator("Coding Beauty");

function startTypingEffect(elementId, generatorInstance) {
  let element = document.getElementById(elementId);
  setInterval(() => {
    let result = generatorInstance.next();
    !result.done && element.innerHTML += result.value;
  }, 100);
}

startTypingEffect("type-here", generatorInstance);
<div id="type-here"></div>

Asynchronous handling

Note: This is NOT the same as being the bedrock of async/await, like we saw earlier. We're talking about async generators here.

function* asyncGenerator() {
  yield new Promise(resolve => setTimeout(() => resolve('Task 1 completed'), 2000));
  yield new Promise(resolve => setTimeout(() => resolve('Task 2 completed'), 3000));
  // ...
}

And here's how we can use this generator:

const asyncGen = asyncGenerator();
asyncGen.next().value.then(console.log);
asyncGen.next().value.then(console.log);

This is such a powerful tool for streaming data in web app in a structured, readable manner -- just look at this function that buffers and streams data for a video-sharing app like YouTube:

async function* streamVideo({ id }) {
  let endOfVideo = false;
  const downloadChunk = async (sizeInBytes) => {
    const response = await fetch(
      `api.example.com/videos/${id}`
    );
    const { chunk, done } = await response.json();
    if (done) endOfVideo = true;
    return chunk;
  };
  while (!endOfVideo) {
    const bufferSize = 500 * 1024 * 1024;
    yield await downloadChunk(bufferSize);
  }
}

To consume this async generator, we'd use the for await..of loop:

for await (const chunk of streamVideo({ id: 2341 })) {
  // process video chunk
}

redux-saga

redux-saga is a library for managing side effects in applications, boasting over 1 million weekly downloads.

Generators play a big role in this library, handling the redux actions to make testing and error handling easier.

Take a look at this simple saga:

import { call, put, takeEvery } from 'redux-saga/effects';

function* fetchData(action) {
   try {
      const data = yield call(Api.fetchUser, action.payload.userId);
      yield put({type: "USER_FETCH_SUCCEEDED", data});
   } catch (e) {
      yield put({type: "USER_FETCH_FAILED", message: e.message});
   }
}

function* mySaga() {
  yield takeEvery("USER_FETCH_REQUESTED", fetchData);
}
import createSagaMiddleware from 'redux-saga';
import { createStore, applyMiddleware } from 'redux';
import mySaga from './sagas';

const sagaMiddleware = createSagaMiddleware();
const store = createStore(
  reducer,
  applyMiddleware(sagaMiddleware)
);
sagaMiddleware.run(mySaga)

Whenever the USER_FETCH_REQUESTED action is dispatched, redux-saga runs the generator which in turn calls fetchData() to perform the asynchronous network request.

A note on return

What happens when you return a value in a generator function? Let's see:

function* soccerPlayers() {
  yield 'Ronaldo';
  yield 'Messi';
  return 'Neymar';
}

for (const player of soccerPlayers()) {
  console.log(player);
}

// Ronaldo
// Messi

Why isn't Neymar part of the generated values?

Let's use .next() to find out if the done property has something to do with it:

function* soccerPlayers() {
  yield 'Ronaldo';
  yield 'Messi';
  return 'Neymar';
}

const playerGen = soccerPlayers();
console.log(playerGen.next());
console.log(playerGen.next());
console.log(playerGen.next());

/*
{ value: 'Ronaldo', done: false }
{ value: 'Messi', done: false }
{ value: 'Neymar', done: true }
*/

It turns out that return is not considered a generated value, so for..of doesn't process it.

Do you remember our very first example:

function* myGenerator() {
  yield 'Hello';
  yield 'World';
}

const iterator = myGenerator();

console.log(iterator.next()); // {value: 'Hello', done: false}
console.log(iterator.next()); // {value: 'World', done: false}
console.log(iterator.next()); // {value: undefined, done: true}

You can see that generators only produce values until, but not including when done is true.

So return completes the generator and terminates the function (like any other).

function* myGenerator() {
    yield 'Hello';
    return 'End';
    yield 'This will not be executed';
}

Final thoughts

JavaScript generators offer powerful solutions for control flow, memory efficiency, and asynchronous handling. They enhance web development with dynamic animations, streaming data, and managing side effects.

Let's embrace the versatility of generators for elegant and efficient JavaScript programming.

See also