Sunday, April 10, 2011

The jQuery module anti-pattern

I don't really know why I'm writing about this now, but it is on my mind and I want to let it go :)
I like jQuery. Boom. I'm sorry if that hurts your feelings. I like it as a neat DOM access layer and basic building block of desktop websites and applications. I'm saying building block, because only jQuery will not get you far, but that is another story.
There is one rule that you have to keep in mind when you are using jQuery and you care about your code to scale beyond a few lines:
You can never use jQuery modules.
jQuery modules (as in methods and properties hanging off the jQuery's prototype object or, to a lesser extent, things hanging off the jQuery function itself) are practically always the wrong design.
  • They all live in a single namespace, so there is risk of collision.
  • They force a DOM centric application design.
  • It almost never looks nice as an API if you are not implementing actions like "show" and "hide".
Instead for almost all use cases where people use jQuery modules it would be better to use a "stratified" API which is this case just means that you pass the jQuery object you want to work on to a function, rather then calling the function on the jQuery object. While jQuery's beauty derives from chaining its basic methods, this usually doen't make sense for more complex modules.
jQuery.sub() has been introduced to somewhat reduce the risk of namespace collision. I think it will go down in history as the most blatant example of a leaky abstraction because you end up never knowing the exact type of a given jQuery object unless you instantiated it yourself.
What about all those nice jQuery modules, you can copy and paste into your application to make fancy things? My personal experience is that most are of very bad quality, so it makes sense to rewrite them to a stratified API style (usually this only take a few minutes) and then assume ownership of the code and fix bugs as they appear.

7 comments:

evilhackerdude said...

tl;dr: jQuery’s ”special” ways makes it very hard to use anything but jQuery.

Most modules and plugins don’t even depend on any given library’s functions BUT on the naming and the behaviour dictated by the library.

Today’s libraries force plugin code to presume that, i.e., the qsa function is $, dojo.query or Y.all. They also must presume that the qsa function might return a nodelist instead of a node – with certain library functions defined in the nodelist prototype by the library (chainability).

This leads to closed module/plugin ecosystems which can only work with the library they’re based upon or even namespaced in.

I would like to see a movement towards smaller libraries with smaller components. What if I really only wanted a good selector engine (i.e. dojo) and a bunch of insanely great fp-inspired functions (underscore)?

For one it’ll need a good loader which uses lexical scope to keep conflicts and dependencies in check.

require([
'dojo/qsa',
'underscore/array/map',
'underscore/object/keys'
]), function(query, map, keys){
// finally, a clean window.
});

We’ll also need libraries which advertise their plugability. Not to win a popularity contest but as a clear message to its users and its maintainers.

Please, we need to go there.

Kris Zyp said...

@evilhackerdude, you may already be aware of the recent changes in Dojo, but 1.6, we switched to AMD format to allow for decoupling of modules from any Dojo-specific environment, and in 1.7 we will breaking the core into sub modules that can be used completely independently of "base" dojo. Hopefully this another step towards a more modular, pluggable future.

evilhackerdude said...

Yes, and very excited about that! But which loader are we going to use in the future and is that going to matter?

Peter van der Zee said...

jQuery.fn === jQuery.prototype

Thank you jQuery for obscuring that fact to many people >.<

That is all.

Jake Archibald said...

Agreed. I think this blurring of DOM/not DOM is going to cause big problems eventually.

Eg, when jQuery UI's Slider triggers a 'slide' event, it also tries to call node.slide() on the element (see the docs for jQuery.fn.trigger). This means if the W3 ever define a method on an HTMLElement called 'slide' things are going to get a bit breaky. This wouldn't happen if the 'slide' event was triggered on a slider instance that wasn't also an HTMLElement.

Another anti-pattern is using string parameters instead of function names, eg $dialog.dialog('open') rather than dialog.open(). The former fails silently if the method name is misspelled and shares a namespace with $dialog.show() which behaves confusingly differently.

gonchuki said...

@Jake:
jQuery UI is a collection of anti-patterns by itself. It's not limited to .slider(), it's the entire API. Like the 'option' magic parameter that's used for dirty function overload.

As for modular stuff, ever tried with MooTools? :) you don't need to plug completely unrelated micro-frameworks when you already have a framework that can be chopped in parts and only include what you will need (in essence, each MooTools module is a micro-framework).

Anonymous said...

@Kris Zyp

Stoked to hear dojo is moving towards AMD, will be awesome to grab only the parts that are needed, and to debug readable code instead of aggregated and minified mumbo-jumbo :)

Big actors populating the js ecosystem with kick-ass AMD modules is a great push for the rest of the community to move in the same direction, and standardizing over something like this will be *massively* benificial.