When I was building a quick frontend to the LLM game, I used jQuery to quickly whip out a prototype. Only after I was happy with it, I ported the code to the modern DOM API. As a result, I totally removed the dependency on jQuery. This whole experience makes me wonder, do people still use jQuery, in this age of frontend engineering? I took some time over the weekend to port one of my old jQuery plugins. This is not intended as a how-to (we have enough of them everywhere), but rather a reflection on how things has changed over time.
A cute illustration generated by Microsoft Copilot
When jQuery Ruled the Web: My Journey Begins
I joined web development in an era where people finally realized JavaScript can do more than just adding fun animations to a page. With the ability to send HTTP request, this opens up a whole new world of interactivity. This newly discovered magic extended the imagination boundary and inspired new dynamic designs.
Previously, these kinds of interactivity could only be achieved with Macromedia/Adobe Flash/Shockwave, requiring a separate runtime installation.
New design patterns emerged, and eventually led to the creation of new JavaScript framework and libraries. In my first job, I was introduced to Yahoo UI (YUI) library at work. jQuery came out slightly later, and it eventually became the most popular library at the time, likely due to the simplistic syntax and efficiency. Sizzle, the query engine powering the library, was touted the best and fastest at the time.
Compared to the Java-like verbosity of YUI, jQuery was designed to get the same tasks done with less code. One thing I really like about the latter is the ability to chain function calls together.
$(element)
.trigger("board:block:init:pre", [context, options])
.addClass("block")
.width(size)
.height(size)
.data("column", col_idx)
.css({
float: "left",
"margin-right": options.margin_px + "px",
"margin-bottom": options.margin_px + "px",
"border-width": options.border_px + "px",
"border-style": "solid",
"border-color": options.border_color,
})
.trigger("board:block:init:post", [context, options]);
In the snippet above, the chain does:
- Trigger a custom event with type board:block:init:pre with additional arguments to be sent to the handler
- Add a class block to the selected element
- Set the width and height of the element
- Set a value to the data property named column
- Set the inline CSS style declaration
- Trigger another custom event with type board:block:init:post with additional arguments to be sent to the handler.
The compactness jQuery brings is still unrivalled, even with the much improved DOM API. We will briefly go through what jQuery does later, using my old jQuery plugin — grid-maker as an example.
At one time, I was building a lot of mini web apps, and they all have one single common element — a grid. You might be wondering, why not Flexbox? It was new at the time, and it seemed to work well, but it also brought more complexity. Even now, I still don’t fully get it, though I completed this cute gamified tutorial.
Anyway, after a few attempts, I revised the code, and decided to extract and reimplement the logic of building a responsive grid as a jQuery plugin. And yes, this library is very well deprecated, so go use a proper CSS grid. You may read this article as a tribute to jQuery’s greatness.
The Allure of Chaining: How jQuery Simplified My Development
Previously, we saw a snippet adapted from the grid-maker plugin. So let’s dive deeper into how do we start coding with jQuery. For DOM manipulation, we usually start by writing a query, following CSS selector rule. Say you want to look for the second div of class board in a div with class hello, you would need the selector div.hello div.board:nth-child(2). Pass the selector as string to the jQuery function (by default aliased to a dollar sign $) to get started, as shown below:
$("div.hello div.board:nth-child(2)")
Now we can do DOM manipulation to the returned jQuery object wrapping the matched element. It can now move around the tree of DOM elements or be populated with other elements. We also saw how the HTML attributes (data-column, class) and inline style could be modified. The one change per function call is likely following the builder pattern. Here comes my favourite feature — the chaining of function calls (many considered this a Fluent Interface). Let’s say we want to adapt the creation of 10 rows from the grid-maker
$(this)
.append(
$.map(_.range(options.row), function (row_idx) {
return $(document.createElement("div"))
}),
)
.append(
$(document.createElement("br")
.css("clear", "both")
)
The last append() could be rewritten as follows to be more concise
.append($.parseHTML('<br style="clear: both;" />'))
Just a side note, $.map functions similar to the new .map() function for arrays. In modern JavaScript the rough equivalent would be
_.range(10) // this underscore method returns a series of 10 numbers in array
.map((row_idx) => document.createElement("div"));
Yes, the jQuery plugin was written in the pre-ES6 era.
The grid-maker was made to be event-driven. Web applications were starting to get more complex (and hence the birth of more capable frameworks like React, Angular, Vue etc) at the time. As the complexity grew, keeping track of things became more difficult. And through the building of the mini web apps, I figured the best strategy would be delegating all the updates to the event listeners. Registering an event listener is easy, just tag a on() call to the chain. Let’s say we want to delegate the further initialization work of each created row to a function called row_init, we can do this
.append(
$.map(_.range(options.row), function (row_idx) {
return $(document.createElement("div"))
.on("board:row:init", row_init)
}),
)
Then we could trigger the actual initialization with trigger(), additional data can be passed to the event handler in the second argument as a list:
.append(
$.map(_.range(options.row), function (row_idx) {
return $(document.createElement("div"))
.on("board:row:init", row_init)
.trigger("board:row:init", [
some,
other,
arguments
]);
}),
)
Though not present in the grid-maker, jQuery was also known for simplifying HTTP requests. It eventually introduced the Promise interface in later versions, similar to the one built into the modern JavaScript now. Yes, it is similar to the new FetchAPI.
$.ajax("https://example.com")
.then(
() => { console.log("success" },
() => { console.error("failure" },
)
Next we will look at the porting process.
Side-by-Side: Unpacking Modern JS vs. jQuery’s Approach
For simpler web applications, I still prefer to keep it simple. Possibly due to my familiarity with jQuery, I still build my first iteration of work with it. Why is that the case? I hear you ask, but hopefully the reason becomes clear as we move along.
We started by selecting an element, let’s emulate jQuery’s behavior, so the solution can be applied to not just 1, but possibly multiple matches. Let’s start with finding the list of elements using document.querySelectorAll, as shown below:
const elems = document.querySelectorAll("div.hello div.board:nth-child(2)");
Then we need to loop through them, this is easy, just chuck everything into a loop:
elems.forEach((board) => { ... })
For each matched board, we populate the rows
_.range(options.row).forEach((row_idx) => {
const elem = document.createElement("div");
const br = document.createElement("br");
br.style.clear = "both";
board.appendChild(elem);
board.appendChild(br);
});
A reference of the row’s div element is kept for event registration purpose, to be shown later. The first major difference would be the losing of (subjectively) elegant chaining syntactic sugar. I also know I could use innerHTML property for the br tag, but I don’t like the idea of injecting raw HTML string into it.
Now, let’s talk about custom events. I decided the library to be event-driven because I wanted to avoid tracing a long list of function calls when a change happens. This is particularly useful when you have multiple elements to be updated whenever something happens. Instead of chaining them together in the event handler, let each individual element subscribe to changes. As an additional benefit, you get a more testable code as the scope gets broken down into smaller units.
Let’s start with event listeners, the equivalent of jQuery’s .on() method in JavaScript is .addEventListener().
elem.addEventListener("board:row:init", row_init);
In order to trigger the event manually, the equivalent of .trigger() would be the .dispatchEvent() method. However, this time, we would need to create a CustomEvent for the purpose. This is where you find the abstraction offered by jQuery valuable.
elem.dispatchEvent(new CustomEvent("board:row:init"));
We needed to pass additional argument just now, but unfortunately, it is done a little bit differently with vanilla JavaScript. Pass the additional data through event.detail by adding a second argument to the CustomEvent instantiation.
elem.dispatchEvent(new CustomEvent(
"board:row:init",
{ detail: {
some,
other,
arguments
}}));
And event handler function would have to be declared differently, as shown below
// jQuery version
function row_init(event, some, other, arguments) { ... }
// vanilla JavaScript version
function row_init(event) {
const {some, other, arguments} = event.detail;
...
}
The new syntactic sugar on object destructuring is used here to somewhat replicate the behaviour.
The FetchAPI equivalent to $.ajax would be somewhat similar for people who are already familiar with jQuery’s Promise object.
fetch("https://example.com")
.then(
() => { console.log("success" },
() => { console.error("failure" },
)
Just for a visual comparison, the screenshot shows the difference of the two implementations.
Different formatters were used so the line count isn’t the best way to determine which is better. However, even with all the new API and syntactic sugar, the vanilla JavaScript version still feels a lot verbose. It is especially evident when it comes to event handling, with the explicit CustomEvent object instantiation. Also losing DOM manipulation methods like appendTo(), prepend(), prependTo() etc. will require developers to rethink the population sequence. Throughout the process of porting legacy code to modern JavaScript, I find this reference titled “You might not need jQuery” useful, especially for people who are looking to “graduate” from jQuery and embrace the new APIs.
There are certainly a lot more to cover when it comes to migrating away from jQuery, as shown in the linked website. The point of this exercise is more about the question we ask in the beginning — do people still code using jQuery now, with all these new toys brought by the recent updates to the API and language itself?
A “Coming of Age” in Code: What My jQuery Journey Taught Me
As mentioned earlier, the idea of this article came from a realization of building prototype with jQuery by default. If I had kept the original revision done purely with jQuery, I wouldn’t have to port grid-maker
to prepare for this article. Having the original and ported code, and comparing them side-by-side is definitely a very interesting experience, considering the original code was written close to a decade ago.
I am still in awe to see the original code still works fine in action today.
Would this exercise change my habit of not starting with jQuery in the future? Likely not. Grabbing jQuery and start working almost feels like a muscle memory deeply ingrained in my body. Or, it could be just because it worked so well and rarely failed me. Heck the prototype application I built still works to this day, what more can I ask?
On the other hand, I certainly see some footprints left by jQuery in the new APIs. They could all be purely coincidence, but it was indeed almost revolutionary at its prime.
If I could make an analogy, I would see this article as a coming of age film, showing the process of us growing out of jQuery in the context of web development. It is still great, but it is likely time to move on. If you are new into the field, may this article serve as a decent introductory to the library.
This article was shaped with the editorial assistance of Gemini. Rest assured, though, that the technical code and the voice telling this story are entirely my own. If my work resonates with you, I’d love to connect for job opportunities or project collaborations. You can reach me here on Medium or find me on LinkedIn.
Top comments (0)