javascript

Mastering JavaScript Method Chaining: A Comprehensive Guide

author

Luis Paredes

published

Aug 5, 2023

When it comes to writing code, being able to write statements clearly has an important positive impact in our productivity because cleaner code is easier to maintain.

One particular JavaScript pattern that helps developers to express logic in a clean way is method chaining.

In this article we'll be digging deeper into how this pattern works so that you can replicate it to made your JavaScript codebase cleaner.

What is method chaining?

As the name suggests, it's a pattern where given an initial object, several methods are chained together to process that initial object until we get the result we want. This creates a sort of pipeline where the output of a method becomes the new object on top of which the next operation in the pipeline will operate.

A typical example where we see this pattern in action is when we want to process strings so that they follow a particular format:

const originalString = "   hello, world!   ";

const result = originalString
  .trim()                     // Remove leading and trailing spaces
  .toLowerCase()              // Convert the string to lowercase
  .replace(",", "")          // Remove commas
  .replace("!", "")          // Remove exclamation marks
  .replace("world", "there"); // Replace "world" with "there"

console.log(result); // Output: "hello there"

In the example above:

  1. We start with the originalString which has leading and trailing spaces
  2. We use .trim() to remove those spaces
  3. We then convert the string to lowercase using .toLowerCase()
  4. We use .replace() to remove commas and exclamation marks from the string
  5. Finally, we use another .replace() to replace the word "world" with "there"

This example demonstrates how you can use method chaining to perform a series of string processing operations in a clear and concise manner. Each method is applied sequentially to the result of the previous method, allowing for a streamlined approach to processing and transforming strings in a clean way.

Notice that the object type doesn't need to be the same throughout the processing sequence, for example we could have an scenario as follows:

const data = [
  { name: "Alice", age: 30 },
  { name: "Bob", age: 25 },
  { name: "Charlie", age: 35 },
];

const result = data
  .filter(person => person.age >= 30)          // Filter people aged 30 and above
  .map(person => `Name: ${person.name}`)        // Map to an array of strings with names
  .join(", ")                                   // Join the array with commas
  .toUpperCase();                               // Convert the entire string to uppercase

console.log(result); // Output: "NAME: ALICE, NAME: CHARLIE"

In this example:

  1. We start with an array of objects called data, where each object represents a person with a name and an age
  2. We use .filter() to filter out people who are aged 30 and above (this operation also returns an array)
  3. We use .map() to transform the filtered array into an array of strings, where each string is formatted as "Name: " (this operation also returns an array)
  4. We use .join() to join the array of strings into a single string, separated by commas (this operation returns a string)
  5. Finally, we use .toUpperCase() to convert the entire string to uppercase (this operation returns a string)

On the other hand, method chaining isn't constrained to sequence of operations that return a specific value. A common use case of the pattern was popularized by jQuery to modify the DOM and even to add animations to an element.

For instance, let's say you have an HTML structure like this:

<!DOCTYPE html>
<html>
<head>
  <script src="https://code.jquery.com/jquery-3.6.0.min.js"></script>
</head>
<body>
  <div id="myDiv">Hello, </div>
</body>
</html>

And you want to use jQuery to manipulate the content of the

element and apply some styling. You can achieve this using method chaining as shown below:

$(document).ready(function() {
  // Method chaining to manipulate content and style
  $("#myDiv")
    .css("color", "blue")
    .append("world!")
    .hide()
    .fadeIn(1000);
});

With this code:

  1. We start by selecting the <div> element with the ID "myDiv" using $("#myDiv")
  2. We use the .css() method to change the text color to blue
  3. We use the .append() method to add the text "world!" to the content of the <div>
  4. We use the .hide() method to hide the <div>
  5. Finally, we use the .fadeIn() method to fade in the <div> over a duration of 1000 milliseconds (1 second)

When you run this code, you'll see the text "Hello, world!" in blue fading into view on the webpage. This is a simple demonstration of how method chaining can make your code more concise and readable when working with jQuery.

Observe that, the key point here is that each jQuery method returns the jQuery object itself, allowing you to chain multiple methods together in a single line.

In the next section, we'll dig deeper into why the type of the returned object in each step of sequence is important for the method chaining pattern to work.

How does method chaining work?

In general, the typical syntax for invoking a method on a specific instance of a certain type in JavaScript follows this pattern:

<instance>.<method>()

The previous expression will be valid, as long as the <method> is one of the methods available for instances of the type of <instance>, thus something like " Hello ".trim() is valid because trim() is a valid method of string (the type of " Hello "), however, something like 1.trim() will throw an error because 1 is of type number and trim() is not a method available for instances of that type.

On the other hand, the initial basic method invocation syntax can be rewritten as follows:

(<instance>).<method>()

The parenthesis surrounding <instance> don't change anything at all, and as long as the expression declared inside those parens returns an instance with the right type, we can write anything we want inside it and the code will work. If we go back to the simple example provided in the previous paragraph, we could, for instance do something like this and the code would still be valid:

function returnString() {
  return " Hello ";
}

returnString().trim();

If we reapply what we learned about the validity of the expression, we could think of the whole returnString().trim() as a string instance because trim() also returns a string. With this in mind, and considering that the string type has many other valid methods, we could take one of those methods and apply it on any string, in particular, we could apply it on the returnString() also returns a string. With this in mind, and considering that the string type has many other valid methods, we could take one of those methods and apply it on any string, in particular, we could apply it on the returnString().trim() string:

(returnString().trim()).<anyStringMethod>() // parens added for clarity

How to implement custom method chaining

When working with functions that take arguments and return a non-undefined value, we can accomplish a similar result as with method chaining using function composition. For instance, we could rewrite the initial example as follows:

const originalString = "   hello, world!   ";

const trim = str => str.trim();
const toLowerCase = str => str.toLowerCase();
const removeCommas = str => str.replace(",", "");
const removeExclamationMarks = str => str.replace("!", "");
const replaceWorld = str => str.replace("world", "there");

const result = replaceWorld(
  removeExclamationMarks(
    removeCommas(
      toLowerCase(
        trim(originalString)
      )
    )
  )
);

console.log(result); // Output: "hello there"

In this case, it is evident that we're still better off using the chained method approach, and if instead of starting with the method chain we receive a codebase using composition, it will be easy for us for the refactor using method chaining because each function returns takes and returns specific types.

On the other hand, there are cases in which the initial object type and the returned type are not obvious, but we could still write cleaner code using chaining using some form of object abstraction.

Take jQuery, for example: under the hood the library works with several types, such as:

However, everything can be neatly handled with method chaining because the creators of the library built a helper object type (the jQuery object type) that elegantly encapsulates the details of working with different types. Let's see how we can also encapsulate logic elegantly using method chaining with an example!

Consider the following HTML structure:

<ul>
  <li id="linkedin">
    <span>https://www.linkedin.com/in/luisalbertoparedes</span>
    <button>Copy</button>
  </li>
  <li id="twitter">
    <span>https://twitter.com/lualdev</span>
    <button>Copy</button>
  </li>
  <li id="gitlab">
    <span>https://gitlab.com/lualparedes</span>
    <button>Copy</button>
  </li>
  <li id="github">
    <span>https://github.com/lualparedes</span>
    <button>Copy</button>
  </li>
</ul>

If we wanted to add the necessary logic to copy the text of each item, we would need to:

  1. Capture the click event
  2. Get the element that contains the text right beside the button that triggered the event
  3. Extract the text from the element
  4. Copy the text to the clipboard

Using a straightforward approach, something like this would work:

const onClick = (event) => {
  const eventTarget = event.target;
  const textElement = eventTarget.parentElement.querySelector("span");
  const text = textElement.innerHTML;

  navigator.clipboard.writeText(text)
    .catch((err) => console.log(err));
};

document.querySelector("body").addEventListener("click", onClick);

However, if we wanted to add more steps to the chain of actions that are triggered by the initial click event, we would have to modify the onClick listener and depending on the complexity of each task we may wind up polluting the function.

If such were the scenario, the code could be refactored as follows to support a method chaining approach for new actions in the list (each method corresponding to an action):

class Operation {
  constructor(event) {
    this.eventTarget = event.target;
  }

  getTextElement() {
    this.textElement = this.eventTarget.parentElement.querySelector("span");
    return this;
  }

  getText() {
    this.text = this.textElement.innerHTML;
    return this;
  }

  copyToClipboard() {
    navigator.clipboard
      .writeText(this.text)
      .catch((err) => console.log(err));
  }
}

const onClick = (e) => {
  new Operation(e).getTextElement().getText().copyToClipboard();
};
document.querySelector("body").addEventListener("click", onClick);

In the refactored code:

  • A helper class Operation was created
  • Each method in the class performs a specific action
  • Every single method in the class returns the instance and as a result of this we can append to the chain other methods from the Operation type

Conclusion

Developing proficiency in method chaining enriches your code's elegance and efficiency. This approach connects data operations, providing a streamlined approach for various tasks. Whether you're modifying strings, handling arrays, or managing complex DOM interactions, method chaining offers a cohesive strategy for achieving clarity and organization.

Integrating method chaining enhances code readability and resilience. I hope this exploration into method chaining extends beyond a technique, opening the door for you to write refined and well-structured JavaScript code.