You're writing JavaScript and suddenly realize with horror and dread that you've written a huge function. It's happened to the best of us. Now comes the almost unstoppable urge to separate the function in smaller pieces.

Big functions mean a lot of responsibilities in one place and, possibly, code smell too, so it's a good idea in itself. But you want to break up the function in such a way that it doesn't lose its meaning or context. To do that, you need to think method chaining - also called function chaining. This article will explain what I mean by that.

Prerequisites

Before we get started, here are a few JavaScript concepts that you already need to be familiar with:

Most experienced developers will have some experience with Array methods, such as map, reduce, and filter. You've probably already come across something like this:

const food = [
  { name: "Banana", type: "fruit" },
  { name: "Apple", type: "fruit" },
  { name: "Chocolate", type: "candy" },
  { name: "Orange", type: "fruit" }
];

// This type of usage is very common
food
  .map(item => item.type)
  .reduce((result, fruit) => {
    result.push(fruit);
    return [...new Set(result)];
  }, []);
// result: ['fruit', 'candy']

Method chaining happens when you want to call multiple functions using the same object and its reference. In the above example, the array method map returns an Array, which has a formidable number of methods.

Because you return a reference pointing to an Array, you'll have access to all the properties of Array.  It's the same principle that you use for your own method chaining.

Better Understanding this

I won't cover all the nuances of this, but it's good to understand why this functions the way it does. No matter how experienced you are, the keyword this can be hard to understand. It's often the reason why things don't work the way you expect them to 😅.

Here's the gist of it: this will always point to the current scope or instance from where it's called. An example:

const dog = {
  is: null,
  log: () => console.log(this.is),
  bark() {
    this.is = "woofing";
    this.log();
    return this;
  },
  walk() {
    this.is = "walking";
    this.log();
    return this;
  },
  eat() {
    this.is = "eating";
    this.log();
    return this;
  }
};

dog
  .bark()
  .eat()
  .walk();

When you write a function like this (no pun intended), you might want to chain methods that are related to the object. The object dog has 3 methods: walk, bark and eat. Every time I call any of its functions, a console should show the representation of what the dog is doing at that exact moment.

But there's a problem with that. If you run it in the browser, you'll realize it doesn't work as expected. That's because arrow functions use lexical scoping, where this refers to its surrounding scope - in this case window, not the object itself.

To resolve that, we should use an anonymous function call to represent the log property:

// instead of this:
  log: () => console.log(this.is),

// use this:
  log() {
    console.log(this.is);
  }

Now the dog's this scope is going to be accessible inside the function.

Using Class

You can achieve the same result using classes:

class Dog {
  is = null;
  log() {
    console.log(this.is);
  }
  bark() {
    this.is = "woofing";
    this.log();
    return this;
  }
  walk() {
    this.is = "walking";
    this.log();
    return this;
  }
  eat() {
    this.is = "eating";
    this.log();
    return this;
  }
}
const dog = new Dog();
dog
  .bark()
  .eat()
  .walk();

Using Prototype

If you have to use prototype, do it this way:

function Dog() {}

Dog.prototype.is = null;
Dog.prototype.log = function() {
  console.log(this.is);
};
Dog.prototype.bark = function() {
  this.is = "woofing";
  this.log();
  return this;
};
Dog.prototype.walk = function() {
  this.is = "walking";
  this.log();
  return this;
};
Dog.prototype.eat = function() {
  this.is = "eating";
  this.log();
  return this;
};
const dog = new Dog();
dog
  .bark()
  .eat()
  .walk();

What About Async Functions?

async functions are synthetic sugar for promises and generators. When you declare an async function, you know that it will return a promise. Because of that, you'll also have access to all the methods of the promise.

const requests = {
  user: null,
  action: null,
  log(something) {
    console.log(this[something]);
  },
  async getUser() {
    this.user = await new Promise(resolve => {
      setTimeout(() => {
        resolve("Douglas Pires");
      }, 1000);
    });
    this.log("user");
    return this;
  },
  async registerAction() {
    this.action = await new Promise(resolve => {
      setTimeout(() => {
        resolve("programming stuff");
      }, 1000);
    });
    this.log("action");
    return this;
  }
};

requests.getUser().then(() => requests.registerAction());

However, it's not a good idea to chain a bunch of promises together. I recommend using a bunch of await keywords instead. It's easier and it will make your code much more readable.

await requests.getUser(); // Douglas Pires
await requests.registerAction(); // programming

And you'll have access to the same properties here:

console.log(requests.user); // Douglas Pires
console.log(requests.action); // programming

In Conclusion

Chaining functions comes in handy when you have to manipulate an object. It's also a practical way to improve the readability of your code. However, as you've seen above, you should chain wisely.

In this article, we've talked about this, classes, prototypes, and async functions in the context of JavaScript method chaining. I hope you've picked up some useful tips and are now better informed about the topic. You can find my repository with all code on JavaScript method chaining right here. See you in the next post!

Enjoyed reading this article? Feel free to share on social media using the buttons below. Alternatively, I've also written about how you can build a color palette generator using Nuxt and Vuetify. Go check it out!