Jquery anti-patterns and best practices

GFX9.COM share Jquery anti-patterns and best practices, you can download now.



A long time ago, in a galaxy far, far away, JavaScript was a hated language. In fact, "hated" is an understatement; JavaScript was a despised language. As a result, developers generally treated it as such, only tipping their toes into the JavaScript waters when they needed to sprinkle a bit of flair into their applications. Despite the fact that there is a whole lot of good in the JavaScript language, due to widespread ignorance, few took the time to properly learn it. Instead, as some of you might remember, standard JavaScript usage involved a significant amount of copying and pasting.

"Don't bother learning what the code does, or whether it follows best practices; just paste it in!" - Worst Advice Ever

Because the rise of jQuery reignited interest in the JavaScript language, much of the information on the web is a bit sketchy.

Ironically, it turns out that much of what the development community hated had very little to do with the JavaScript language, itself. No, the real menace under the mask was the DOM, or "Document Object Model," which, especially at the time, was horribly inconsistent from browser to browser. "Sure, it may work in Firefox, but what about IE8? Okay, it may work in IE8, but what about IE7?" The list went on tirelessly!

Luckily, beginning around five years ago, the JavaScript community would see an incredible change for the better, as libraries like jQuery were introduced to the public. Not only did these libraries provide an expressive syntax that was particularly appealing to web designers, but they also managed to level the playing feel, by tucking the workarounds for the various browser quirks into its API. Trigger $.ajax and let jQuery do the hard part. Fast-forward to today, and the JavaScript community is more vibrant than ever - largely due to the jQuery revolution.

Because the rise of jQuery reignited interest in the JavaScript language, much of the information on the web is a bit sketchy. This is less due to the writers' ignorance, and more a result of the fact that we were all learning. It takes time for best practices to emerge.

Luckily, the community has matured immensely since those days. Before we dive into some of these best practices, let's first expose some bad advice that has circulated around the web.


Don't Use jQuery

The problem with tips like this is that they take the idea of pre-optimization to an extreme.

Much like Ruby on Rails, many developers' first introduction to JavaScript was through jQuery. This lead to a common cycle: learn jQuery, fall in love, dig into vanilla JavaScript and level up.

While there's certainly nothing wrong with this cycle, it did pave the way for countless articles, which recommended that users not use jQuery in various situations, due to "performance issues." It wouldn't be uncommon to read that it's better to use vanilla for loops, over $.each. Or, at some point or another, you might have read that it's best practice to use document.getElementsByClassName over jQuery's Sizzle engine, because it's faster.

The problem with tips like this is that they take the idea of pre-optimization to an extreme, and don't account for various browser inconsistencies - the things that jQuery fixed for us! Running a test and observing a savings of a few milliseconds over thousands of repetitions is not reason to abandon jQuery and its elegant syntax. Your time is much better invested tweaking parts of your application that will actually make a difference, such as the size of your images.


Multiple jQuery Objects

This second anti-pattern, again, was the result of the community (including yours truly at one point) not fully understanding what was taking place under the jQuery hood. As such, you likely came across (or wrote yourself) code, which wrapped an element in the jQuery object countless times within a function.

$('button.confirm').on('click', function() {
    // Do it once
    $('.modal').modal();

    // And once more
    $('.modal').addClass('active');

    // And again for good measure
    $('modal').css(...);
});

While this code might, at first, appear to be harmless (and truthfully is, in the grand scheme of things), we're following the bad practice of creating multiple instances of the jQuery object. Every time that we refer to $('.modal'), a new jQuery object is being generated. Is that smart?

Think of the DOM as a pool: every time you call $('.modal'), jQuery is diving into the pool, and hunting down the associated coins (or elements). When you repeatedly query the DOM for the same selector, you're essentially throwing those coins back into the water, only to jump in and find them all over again!

Always chain selectors if you intend to use them more than once. The previous code snippet can be refactored to:

$('button.confirm').on('click', function() {
    $('.modal')
        .modal()
        .addClass('active')
        .css(...);
});

Alternatively, use "caching."

$('button.confirm').on('click', function() {
    // Do it ONLY once
    var modal = $('.modal');

    modal.modal();
    modal.addClass('active');
    modal.css(...);
});

With this technique, jQuery jumps into the DOM pool a total of one time, rather than three.


Selector Performance

Too much attention is paid to selector performance.

While not as ubiquitous these days, not too long ago, the web was bombarded by countless articles on optimizing selector performance in jQuery. For example, is it better to use $('div p') or $('div').find('p')?

Ready for the truth? It doesn't really matter. It's certainly a good idea to have a basic understanding of the way that jQuery's Sizzle engine parses your selector queries from right to left (meaning that it's better to be more specific at the end of your selector, rather than the beginning). And, of course, the more specific you can be, the better. Clearly, $('a.button') is better for performance than $('.button'), due to the fact that, with the former, jQuery is able to limit the search to only the anchor elements on the page, rather than all elements.

Beyond that, however, too much attention is paid to selector performance. When in doubt, put your trust in the fact that the jQuery team is comprised of the finest JavaScript developers in the industry. If there is a performance boost to be achieved in the library, they will have discovered it. And, if not them, one of the community members will submit a pull request.

With this in mind, be aware of your selectors, but don't concern yourself too much with performance implications, unless you can verbalize why doing so is necessary.


Callback Hell

jQuery has encouraged widespread use of callback functions, which can certainly provide a nice convenience. Rather than declaring a function, simply use a callback function. For example:

$('a.external').on('click', function() {
    // this callback function is triggered
    // when .external is clicked
});

You've certainly written plenty of code that looks just like this; I know I have! When used sparingly, anonymous callback functions serve as helpful conveniences. The rub occurs down the line, when we enter... callback hell (trigger thunderbolt sound)!

Callback hell is when your code indents itself numerous times, as you continue nesting callback functions.

Consider the following quite common code below:

$('a.data').on('click', function() {
    var anchor = $(this);
    $(this).fadeOut(400, function() {
        $.ajax({
            // ...
            success: function(data) {
                anchor.fadeIn(400, function() {
                    // you've just entered callback hell
                });
            }
        });
    });
});

As a basic rule of thumb, the more indented your code is, the more likely that there's a code smell. Or, better yet, ask yourself, does my code look like the Mighty Ducks Flying V?

When refactoring code such as this, the key is to ask yourself, "How could this be tested?" Within this seemingly simple bit of code, an event listener is bound to a link, the element fades out, an AJAX call is being performed, upon success, the element fades back in, presumably, the resulting data will be appended somewhere. That sure is a lot to test!

Wouldn't it be better to split this code into more manageable and testable pieces? Certainly. Though the following can be optimized further, a first step to improving this code might be:

var updatePage = function(el, data) {
    // append fetched data to DOM
};

var fetch = function(ajaxOptions) {
    ajaxOptions = ajaxOptions || {
        // url: ...
        // dataType: ...
        success: updatePage
    };

    return $.ajax(ajaxOptions);
};

$('a.data').on('click', function() {
    $(this).fadeOut(400, fetch);
});

Even better, if you have a variety of actions to trigger, contain the relevant methods within an object.

Think about how, in a fast-food restaurant, such as McDonalds, each worker is responsible for one task. Joe does the fries, Karen registers customers, and Mike grills burgers. If all three members of the staff did everything, this would introduce a variety of maintainability problems. When changes need to be implemented, we have to meet with each person to discuss them. However, if we, for example, keep Joe exclusively focused on the fries, should we need to adjust the instructions for preparing fries, we only need to speak with Joe and no one else. You should take a similar approach to your code; each function is responsible for one task.

In the code above, the fetch function merely triggers an AJAX call to the specified URL. The updatePage function accepts some data, and appends it to the DOM. Now, if we want to test one of these functions to ensure that it's working as expected, for example, the updatePage method, we can mock the data object, and send it through to the function. Easy!


Reinventing the Wheel

It's important to remember that the jQuery ecosystem has matured greatly over the last several years. Chances are, if you have a need for a particular component, then someone else has already built it. Certainly, continue building plugins to increase your understanding of the jQuery library (in fact, we'll write one in this article), but, for real-world usage, refer to any potential existing plugins before reinventing the wheel.

As an example, need a date picker for a form? Save yourself the leg-work, and instead take advantage of the community-driven - and highly tested - jQuery UI library.

Once you reference the necessary jQuery UI library and associated stylesheet, the process of adding a date picker to an input is as easy as doing:



Or, what about an accordion? Sure, you could write that functionality yourself, or instead, once again, take advantage of jQuery UI.

Simply create the necessary markup for your project.

Chapter 1

Some text.

Chapter 2

Some text.

Chapter 3

Some text.

Section 4

Some text.

Then, automagically turn it into an accordion.

$(function() {
    $("#accordion").accordion();
});

What if you could create tabs in thirty seconds?

Create the markup:

About us text.

Our mission text.

Get in touch text.

And activate the plugin.

$(function() {
    $("#tabs").tabs();
});

Done! It doesn't even require any notable understanding of JavaScript.


Plugin Development

Let's now dig into some best practices for building jQuery plugins, which you're bound to do at some point in your development career.

We'll use a relatively simple MessageBox plugin as the demo for our learning. Feel free to work along; in fact, please do!

The assignment: implement the necessary functionality to display dialog boxes, using the syntax, $.message('SAY TO THE USER'). This way, for example, before deleting a record permanently, we can ask the user to confirm the action, rather than resorting to inflexible and ugly alert boxes.


Step 1

The first step is to figure out how to "activate" $.message. Rather than extending jQuery's prototype, for this plugin's requirements, we only need to attach a method to the jQuery namespace.

(function($) {
    $.message = function(text) {
    console.log(text);
  };
})(jQuery);

It's as easy as that; go ahead, try it out! When you call $.message('Here is my message'), that string should be logged to the browser's console (Shift + Command + i, in Google Chrome).


Step 2

While there's not enough room to cover the process of testing the plugin, this is an important step that you should research. There is an amazing sense of assuredness that occurs, when refactoring tested code.

For example, when using jQuery's test suite, QUnit, we could test-drive the code from Step 1 by writing:

 module('jQuery.message', {
    test('is available on the jQuery namespace', 1, function() {
        ok($.message, 'message method should exist');
    });
 });

The ok function, available through QUnit, simply asserts that the first argument is a truthy value. If the message method does not exist on the jQuery namespace, then false will be returned, in which case the test fails.

Following the test-driven development pattern, this code would be the first step. Once you've observed the test fail, the next step would be to add the message method, accordingly.

While, for brevity's sake, this article will not delve further into the process of test-driven development in JavaScript, you're encouraged to refer to the GitHub repo for this project to review all the tests for the plugin: https://github.com/JeffreyWay/MessageBox/blob/master/test/MessageBox_test.js


Step 3

A message method that does nothing isn't of help to anybody! Let's take the provided message, and display it to the user. However, rather than embedding a huge glob of code into the $.message method, we'll instead simply use the function to instantiate and initialize a Message object.

(function($) {
    "use strict";

    var Message = {
        initialize: function(text) {
            this.text = text;

            return this;
        }
    };

    $.message = function(text) {
        // Needs polyfill for IE8--
        return Object.create(Message).initialize(text);
    };
})(jQuery);

Not only does this approach, again, make the Message object more testable, but it's also a cleaner technique, which, among other things, protects again callback hell. Think of this Message object as the representation of a single message box.


Step 4

If Message represents a single message box, what will be the HTML for one? Let's create a div with a class of message-box, and make it available to the Message instance, via an el property.

var Message = {
    initialize: function(text) {
        this.el = $('
', { 'class': 'message-box', 'style': 'display: none' }); this.text = text; return this; } };

Now, the object has an immediate reference to the wrapping div for the message box. To gain access to it, we could do:

var msg = Object.create(Message).initialize();

// [
​
​] console.log(msg.el);

Remember, we now have an HTML fragment, but it hasn't yet been inserted into the DOM. This means that we don't have to worry about any unnecessary reflows, when appending content to the div.


Step 5

The next step is to take the provided message string, and insert it into the div. That's easy!

initialize: function(text) {
    // ...
    this.el.html(this.text);
}

// [
​Here is an important message​
​]

However, it's unlikely that we'd want to insert the text directly into the div. More realistically, the message box will have a template. While we could let the user of the plugin create a template and reference it, let's keep things simple and confine the template to the Message object.

 var Message = {
    template: function(text, buttons) {
        return [
            '

' + text + '

', '
', buttons, '
' ].join(''); // ... };

In situations, when you have no choice but to nest HTML into your JavaScript, a popular approach is to store the HTML fragments as items within an array, and then join them into one HTML string.

In this snippet above, the message's text is now being inserted into a paragraph with a class of message-box-text. We've also set a place for the buttons, which we'll implement shortly.

Now, the initialize method can be updated to:

initialize: function(text) {
    // ...
    this.el.html(this.template(text, buttons));
}

When triggered, we build the structure for the message box:


								

Similar content