Unleashing the power of JavaScript generators: from basics to real-world applications (complete guide)

Last updated on July 03, 2023
Unleashing the power of JavaScript generators: from basics to real-world applications (complete guide)

Let's talk about generators...

Generators, you ask?

Well, they're these special functions in JavaScript...

They give you more control over the function execution process.

It's like having a magic wand for your code!

You might not have used them much, or at all.

Perhaps you've been coding without realizing their potential.

Or maybe they seemed a little intimidating or unnecessary at first glance.

No worries, we've all been there.

But once you unlock the magic of generators, there's no turning back.

You'll find them incredibly useful in a variety of coding scenarios.

So, are you ready to journey into the world of JavaScript generators?

Let's dive right in!

Deep dive into generators

To kickstart our journey let's look at what's so unique about generators.

Well unlike regular functions generators pause and resume their execution.

Intriguing isn't it?

Let's look at a quick 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}

This example paints a clear picture of what's happening.

Notice how the keyword function* is used to define a generator.

That * marks our function as a generator.

You can also see the yield keyword.

yield is how we pause execution in a generator.

Each time yield is encountered, the generator gives a value back.

This value can be accessed using the .next() method.

.next() is a powerful method provided by generators.

In the example, iterator.next() is called three times.

The first two calls to next() return 'Hello' and 'World' respectively.

On the third call, the generator indicates it's done and returns undefined.

It's as if the generator is saying: "That's it, friend. I have nothing more to give."

By now, you might be realizing that generators are pretty cool.

Indeed, they are!

In the next section, we'll dig into why they are so beneficial.

Benefits of using generators

Lazy evaluation

Lazy evaluation is a core benefit of generators.

Simply put, it means "calculate only when necessary."

In contrast, regular functions in JavaScript 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;
  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 more efficient:

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

const bigNumbers = bigNumberGenerator();

// Generate numbers as you need them, no need to store a million numbers in memory!

Handling asynchronous tasks more efficiently

Asynchronous code can get messy.

Promises and async/await help, but generators add another tool to our toolbox.

They can pause and resume, perfect for handling asynchronous operations.

function* fetchUser(userId) {
  const response = yield fetch(`https://api.example.com/users/${userId}`);
  const user = yield response.json();
  return user;
}

In this code, the fetchUser generator fetches a user from an API.

It doesn't move to the next yield until it has the result, keeping your async code clean and readable.

Real-world application: simulating a typing animation

Isn't it mesmerizing to see typing animations on websites? They hold our attention and enhance the user experience.

Now, creating a typing animation has its challenges.

Traditional ways can be complex and a bit convoluted.

You've probably seen code like this:

let text = 'Hello, world!';
let index = 0;

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

typeAnimation();

The above function creates a typing effect, but it's not very flexible.

You can't easily control when it starts and stops or when it resumes.

Here's where generators can simplify things for us.

Yes, generators, those special functions in JavaScript that can pause and resume their execution!

The beauty of generators is their ability to produce data on demand.

They don't compute all values upfront.

Instead, they generate them when needed.

Let's see how we can use a generator to make our typing animation.

We'll create a generator function that yields a new character from our string each time it's called.

Something like this:

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

Imagine the possibilities with this generator function!

You now have more control over the typing animation.

In the next section, we'll see how to put this generator to work.

We'll step by step build our typing animation, using the power of generators.

Step-by-step guide to creating a typing animation using generators

Begin.

Create a new HTML file. Here's the base layout:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Typing Animation</title>
</head>
<body>
  <div id="type-here"></div>
  <script src="index.js"></script>
</body>
</html>

Notice the type-here div? That's where our animation goes.

Also, we've linked to index.js. That's our JavaScript file.

Defining the generator

JavaScript time.

Let's create a generator function. Name it typeGenerator.

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

What's happening here?

Our generator goes through each character in the text.

Then, it "yields" the character.

Easy, right?

Implementing the animation

Let's animate.

Create a generator instance with your message.

const generatorInstance = typeGenerator("Coding Beauty");

Then, initiate a typing effect function. It will use our generator instance.

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

And call the function:

startTypingEffect("type-here", generatorInstance);
A typing effect created using JavaScript generators.

Success!

Now, let's explore more applications.

Further applications of generators

Asynchronous handling

Generators are pretty good at handling asynchronous tasks.

Running some action while allowing other code to run...

With generators, you can do this in a simpler way.

Here's a code example:

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);  // Isn't this neat?
asyncGen.next().value.then(console.log);

Data streaming

Next up, generators are very helpful for data streaming.

Say you're working with large datasets.

Often, you don't need to process all the data at once.

With generators, you can handle data bit by bit.

This is called 'lazy evaluation'.

Here's a generator that does this:

function* dataStreamer(data) {
  for (let item of data) {
    yield item;
  }
}

Let's see how we can consume this generator:

const largeData = Array(1000000).fill().map((_, i) => i);
const streamer = dataStreamer(largeData);
for(let value of streamer) {
  // process value, it's as simple as that!
}

redux-saga

Did you know that generators have a big role in redux-saga?

redux-saga is a library for managing side effects in applications.

In redux-saga, actions are handled by generators.

This makes 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);
}

In redux-saga, the saga middleware runs the generators when actions are dispatched:

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);  // And it's done!

In summary, generators make asynchronous operations in JavaScript simpler.

They can help optimize memory use when dealing with data streaming.

And in redux-saga, they help manage side effects in an efficient way.

So, why not try using generators in your next project?

Limitations and precautions when using generators

Caution ahead!

Yes, generators are powerful.

But they're not omnipotent.

They have their limitations.

For instance, they don't work well with 'return' statements.

Consider this code:

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

Calling next() on myGenerator stops at 'End'.

The third yield statement isn't reached.

Now you know why!

Generator pitfalls

It's easy to get caught in a forever loop.

For example:

function* infiniteGenerator() {
    let index = 0;
    while (true) {
        yield index++;
    }
}

Use infiniteGenerator with caution. Or your code could run indefinitely!

Beware of Promises...

Generators and asynchronous operations can get tricky.

For example:

function* asyncGenerator() {
    const response = yield fetch('https://api.example.com/data');
    return response.json();
}

This code won't work as expected.

Generators don't implicitly handle Promises.

But don't worry! Libraries like co can help.

They let generators and Promises play nicely.

Embrace these cautions and become a master of generators.

Final thoughts

Wrap-up.

Generators, unique.

Yield, powerful.

No more pulling everything at once, just piecemeal.

Need more data? .next() is your friend.

Remember:

function* typingGenerator(str) {
    for(let char of str) {
        yield char;
    }
}

Easy, elegant.

It's JavaScript's brilliance.

Generator's versatility? Undeniable. We've only scraped the surface.

Expand your horizons.

Asynchronous tasks? Data streaming? Side effects management in Redux-Saga?

Yes, generators work.

But remember, limitations exist. They don't fit everywhere. Be cautious.

Explore more. Experiment. Push your code's boundaries. Unleash Generators' power.

Keep learning.

Onwards to more JavaScript adventures!

11 Amazing New Features in ES13

11 Amazing New Features in ES13
Get up to speed with all the latest features added in ECMAScript 13 to modernize your JavaScript with shorter and more expressive code.

See also