jQuery Critique
Jakob Jenkov |
From around 2013 and forward jQuery has received some criticism from developers using jQuery for larger projects. The critique is usually centered around two problems:
- jQuery leads to the "big main method problem".
- jQuery leads to DOM centric code.
These two problems are also sometimes referred to as the "jQuery soup". In this text I will explain what these problems are, and also talk a bit about how to avoid these problems.
The jQuery Big Main Method Problem
One of the bigger critique points against jQuery is what has been called the "big main method problem". In this section I will dig deeper into this critique point and explain what the big main method problem is.
jQuery recommends that you wait until the page is loaded until you start modifying the DOM, attaching event listeners
etc. This is done by putting your page initialization code inside the callback function passed to
jQuery's $(document).ready();
function. Here is an example of how that looks:
$(document).ready(function() { $("#tab1").click(function() { }); $("#tab2").click(function() { }); $("#tab3").click(function() { }); $("#submitButton").click(function() { }); });
This example waits for the document to be ready (loaded), and then executes the function passed to jQuery's
$(document).ready()
function. Inside the callback function passed as parameter, all of the page
initialization takes place.
The problem is, that if the page is big and has complex behaviour, the amount of code inside the
$(document).ready( ... )
function grows quite large. Since this is where the page is initialized,
that looks similar to the main()
method of a C
or Java
application.
Hence the name "Big Main Method Problem". A big main method is hard to navigate and thus harder to maintain.
All the code is located inside the same function. The critique is fully valid.
The solution is to divide the page initialization code up into smaller functions, or even put them into
JavaScript objects ("modules") which have different functions which can then be called from the $(document).ready()
function. However, jQuery does not give you a standard way of doing this. You have to come up with a way yourself.
Additionally, the code inside the $(document).ready()
function is often a mix of GUI component logic
and application specific logic. jQuery also does not give you a good way of separating these two types of logic.
It is not impossible to separate these two kinds of logic, but you have to come up with your own method. Personally
I use the same method as for the page initialization code. I put GUI logic code into its own functions (or JavaScript
objects - "components").
Without help to separate the page initialization logic into the different parts of the page they are initializing, and help to separate GUI logic from application logic, it is easy to end up with "The Big Main Method Problem" when using jQuery. This is one of the reasons I developed jqComponents - a framework used on top of jQuery that helps you separate your code into components and modules.
jQuery DOM Centric Code
Another problem that jQuery is often criticized for is that jQuery leads to DOM centric code. It is true that using jQuery for web applications can result in DOM centric code. In some cases this is caused by jQuery conventions, and in some cases this is caused by the developer. In this section will look at both cases, and look at how to avoid writing DOM centric code with jQuery.
What is DOM Centric Code?
Let me first explain what "DOM centric code" means. DOM centric code is code that is modeled around the DOM. DOM centric code tends to attach both functionality and data of both GUI components and application modules to the DOM, rather than keeping them in separate JavaScript objects. Thus, the DOM becomes the "spine" of the application. In other words, the DOM is used to store non-DOM related aspects of the application, like state and behaviour (data and functions).
The problem arises when you start using the DOM to contain information or behaviour that is actually unrelated to the DOM itself, but instead related to your application. For instance, when you turn DOM elements into GUI components, or data structures. Look at this example:
<style> .toggleButton { font-weight: bold; } </style> <button id="button1" class="toggleButton">Button 1</button> <button id="button2" class="toggleButton">Button 2</button> <script> $(document).ready(function() { $(".toggleButton").click(function() { $(this).toggleClass("buttonActivated"); }); }); </script>
This example contains two button elements. The jQuery code selects these two buttons based on their CSS
class and attaches a click listener to them. The click listener toggles (adds / removes) another CSS
class called buttonActivated
on the button. Using jQuery you can now check if a button is
"activated" by checking if the button element contains the CSS class "buttonActivated. Here is how
you can do that:
var isButtonActivated = $("#button1").hasClass("buttonActivated");
And you can select all activated buttons like this:
var activatedButtons = $(".buttonActivated");
This is when the problems of DOM centric code starts emerging. The CSS class buttonActivated
represents
a state, namely whether or not that button is activated or not. Of course, the state of a button should
perhaps be reflected in its visual style, so by itself there is nothing wrong with attaching a CSS class to a button
depending on the button's state. It is when you use the CSS class to obtain the button's state that the problems
arise. When the CSS class is used for something else than visual styling. And that is what the two last examples did.
They used the presence of the CSS class buttonActivated
to read the state of the buttons.
DOM centricity is not isolated to CSS classes. Whenever you are setting CSS classes, HTML attributes or attaching
data to HTML elements using jQuery's .data()
function which does not have any explicit DOM function,
you are creating DOM centric code. In other words, when you are using the DOM for non-DOM purposes, you are writing
DOM centric code.
In a traditional object oriented GUI application you would use your own object hierarchy as the spine of the application, and not the hierarchy of the GUI components. You would not attach application specific logic and data to the GUI components like you often do with the DOM via jQuery.
Why Is DOM Centric Code Bad?
The problem with DOM centric code is that the code gets harder to read and maintain, and thus easier to break. Combined with the "big main method" problem, you can end up with an application that feels messy and is really hard to continue developing.
So, how does DOM centric code lower the readability of the code? It does, by hiding the double meanings of CSS classes, HTML attributes or attached data. You cannot easily see that a CSS class is used for more than styling an HTML element. If you decide to split the CSS class up into multiple classes, or just rename it, you may break code that relies on selecting elements with that CSS class. Since CSS classes are often defined in an external CSS style sheet, you may not realize how many files are referring to the that class for other purposes than styling (e.g. marking if a button is activated or not).
DOM centric code also makes it harder to see what the total behaviour of some DOM element is, after being modified. This is typically the case if you are turning a set of DOM elements into something resembling GUI components. Look at this example:
$("#theDiv").click(function() { $(this).toggleClass("divActivated"); }); $("#theDiv").hover( function() { $(this).addClass("divHighlighted"); }, function() { $(this).removeClass("divHighlighted"); } ); $("#theDiv button.help").click(function() { $("#helpDiv").html("Activating theDiv results in ...").show(); });
This example attaches behaviour to a div
element. When you click the div
element
it becomes "activated" (has the CSS class divActivated
attached to it), and if you click it
again the div
becomes "deactivated". Second the example adds hover
behaviour
too which highlights the div
if the mouse hovers above it (attaches the CSS class divHighlighted
).
Third, the example sets a click listener on any nested button
element with the CSS class help
.
The example is effectively turning a div
element into a GUI component - a button which the user can
activate and deactivate by clicking it. It is not that easy, however, to see from the code alone what kind of GUI
component these instructions create. You will have to read the code carefully to learn its bigger meaning.
Of course you could improve the code by grouping these instructions inside a function, like this:
function createToggleButton(elementSelector) { $(elementSelector).click(function() { $(this).toggleClass("divActivated"); }); $(elementSelector).hover( function() { $(this).addClass("divHighlighted"); }, function() { $(this).removeClass("divHighlighted"); } ); $(elementSelector + " button.help").click(function() { $("#helpDiv").html("Activating theDiv results in ...").show(); }); }
The name of the function now tells what the instructions do, and you can reuse the code with different elements. Just call the functions multiple times, like this:
createToggleButton("#theDiv1"); createToggleButton("#theDiv2");
You could also turn the createToggleButton()
function into a jQuery plugin.
You would then call it like this:
$("#theDiv1").createToggleButton(); $("#theDiv2").createToggleButton();
However, both the function and the jQuery plugin solution have a major drawback. The only contain part of the full behaviour of a full toggle button implementation. What if you need to be able to read the activation state of the button? Or, if you need to activate or deactivate the button from your JavaScript code? You would need additional code.
To read the state of a toggle button you would need this code:
$("#theDiv1").hasClass("divActivated");
To set the state of a toggle button you would need this code:
$("#theDiv1").addClass("divActivated"); //activate toggle button $("#theDiv1").removeClass("divActivated"); //deactivate toggle button
Again, you could move this code inside a function or plugin to make it easier to understand what the code does, logically. Here is an example:
function isToggleButtonActive(elementSelector) { return $(elementSelector).hasClass("divActivated"); } function setToggleButtonActivationState(elementSelector, activated) { if(activated) { $(elementSelector).addClass("divActivated"); } else { $(elementSelector).removeClass("divActivated"); } }
These functions could then be called like this:
var isActive = isToggleButtonActive("#theDiv1"); setToggleButtonActivationState("#theDiv1");
However, now the toggle button GUI component has its implementation spread out over three functions. If you look inside a single function, it is not easy to know that there are two more functions which augment the behaviour of the toggle button.
If you were to turn the functions into jQuery plugins you would need to create 3 plugins. Or, as some jQuery plugins
do, or take "function" parameters to the same plugin function and based on the function parameter call do different
things. Here is an example of calling a toggleButton
jQuery plugin that does that:
$("#theDiv1").toggleButton( { action : "init" } ); $("#theDiv1").toggleButton( { action : "setState", activated : true } );
I am sure you can imagine how messy the toggleButton
jQuery plugin function becomes in order to support
this. Additionally, reading function calls encoded as JavaScript parameter objects is also harder than reading standard
function calls.
Finally, since the the convention for jQuery plugins is to return the selection (element or elements) the plugin is applied to, you cannot return other values from a jQuery plugin without breaking this convention. Breaking this convention will certainly confuse some developers who are used to the convention. The solution could be to provide a "return value" property in the parameter object passed to the plugin. For instance:
var param = { action : "isActive", activated : null }; $("#theDiv1").toggleButton( param ); if(param.activated) { ... }
Then the toggleButton
jQuery plugin could return the activation state in the activated
field of the parameter object, and still return the selected element from the plugin function itself.
But then your plugin and the code using it is starting to get really messy.
jQuery Plugins Clutter Your Code
At the time of writing, jQuery plugins is the standard way of modularizing, sharing and reusing DOM manipulation code, whether it is implementing simple DOM manipulation or more advanced GUI components. When used to encapsulate simple DOM manipulation, jQuery plugins can work just fine.
But when implementing GUI components consisting of one or more DOM elements, component state (variables) and component behaviour (functions), jQuery plugins are a bad choice.
In order to store state variables associated with a GUI component, a jQuery plugin
will often attach the state as data to the GUI component's DOM element using jQuery's .data()
function.
This is a classic example of DOM centric code. In order to get to the GUI components state you need to go through
the jQuery plugin, and in order to activate the jQuery plugin you first need to select the DOM element, because
jQuery plugins are executed on DOM elements.
The same is true for functions associated with a GUI component. In order to call a function on a GUI component via a jQuery plugin, you will first have to select the DOM element, call the jQuery plugin and pass some parameter object to the jQuery plugin function specifying what function to call. I have shown that earlier. It's hard to read and clumsy to work with.
jQuery plugins may seem like one of jQuery's biggest blessings, but it is also one of its biggest curses. Use them with extreme care, or better yet: Don't use plugins at all. You don't need them. There are other, smarter and just as simple ways to implement GUI components. I will get back to that later in this text.
Not All DOM Centric Code is Bad
Not all DOM centric code is bad. jQuery is all about DOM manipulation and of course basic DOM manipulation is inherently DOM centric. Look at this example:
$(".article").addClass("highlight").show();
This example selects all HTML elements with the CSS class article
, adds another CSS class to them
called hightlight
, and finally shows all the HTML elements. Since this example performs basic
DOM manipulation it is naturally DOM centric. As long as your code does basic DOM manipulation the DOM centricity
is not a problem. It is when you mix non-DOM manipulating responsibility into your DOM manipulating code that
the problems of DOM centric code occurs.
How Do You Avoid DOM Centric Code?
The simplest way to avoid DOM centric code is to group your JavaScript code into JavaScript objects. Especially your DOM manipulating code. Here is how the toggle button shown earlier in this text would look as a JavaScript object:
function toggleButton(elementSelector, helpDivSelector) { var button = { }; button.activated = false; button.element = $(elementSelector); button.element.click(function() { button.setActivationState(!button.activated); }); button.element.hover( function() { button.element.addClass("divHighlighted"); }, function() { button.element.removeClass("divHighlighted"); } ); $(elementSelector + " button.help").click(function() { $(helpDivSelector).html("Activating theDiv results in ...").show(); }); button.isActivated = function() { return button.activated; } button.setActivationState = function(activated) { button.activated = activated; if(button.activated) { button.element.addClass("divActivated"); } else { button.element.removeClass("divActivated"); } } return button; }
This example shows a factory function called toggleButton
which creates a JavaScript object,
attaches an HTML element and an activation state to it, and returns the JavaScript object.
You can see immediately how the whole toggle button implementation can fit into the same JavaScript object. It contains a state, a reference to an HTML element and two functions. Additionally the configuration of the HTML element listeners etc. are embedded inside the factory function for the JavaScript object. Everything is contained here.
Using the JavaScript object returned by the toggleButton()
factory function would look like this:
var toggleButton1 = toggleButton("#theDiv1", "#helpDiv"); var toggleButton2 = toggleButton("#theDiv2", "#helpDiv"); toggleButton1.setActivationState(true); toggleButton2.setActivationState(false); var isButton1Activated = toggleButton1.isActive(); var isButton2Activated = toggleButton2.isActive();
This code is much easier to understand than what you have seen earlier. It is also easier to maintain and reuse.
Notice also how the direction of control has been reversed. When you use jQuery alone, or jQuery plugins, you start
by selecting a DOM element, and then do something with it. When using JavaScript objects you start by creating
a JavaScript object, attach an HTML element plus some internal state to it, and then refer to the JavaScript object
for all future manipulation of the GUI component and corresponding HTML elements. All data is kept inside the JavaScript
object, not in HTML attributes, CSS classes or attached to the HTML elements using jQuery's .data()
function. Thus, you do not need to access the DOM to read the GUI component's state.
I refer to JavaScript objects that embeds jQuery DOM manipulation as "jQuery components". You can take the idea of jQuery components much further than what I have shown here. I have done that in my jQuery based GUI framework jqComponents.
The framework helps you divide your code between reusable GUI components and application specific modules. It also contains a set of built-in components so you don't have to implement all the basic components yourself. Among these components are a table component for displaying JavaScript objects in tables, a tree component, a panel deck which can be used to implement slide shows or tabbed panes, and a JavaScript based responsive layout manager which lets you do more avanced responsive design than you can do with CSS alone. The framework website also has a custom build system so you don't have to include all the framework's code if you don't need it.
Summary
Used with consideration, jQuery is still the king of DOM manipulation, but you should do what you can do avoid writing DOM centric code. To sum it up, you should:
- Group your DOM manipulation code into JavaScript objects.
- Don't use jQuery plugins except for basic, stand-alone DOM manipulation. In fact, just stay away from jQuery plugins completely. JavaScript objects are superior to jQuery plugins. That unfortunately includes statying away from frameworks based on jQuery plugins, like jQuery UI.
- Don't use jQuery's
.data()
function unless you have no other choice.
Signs that your code is becoming DOM centric is that it:
- Attaches state to DOM elements
- You access the state of GUI component via HTML elements.
- Uses jQuery plugins
- Does not encapsulate DOM manipulation code inside JavaScript objects.
Tweet | |
Jakob Jenkov |