8. The jQuery Library
jQuery is the JavaScript library that made the DOM, events and AJAX of Chapters 5 and 6 usable across the inconsistent browsers of the late 2000s, and its ideas (CSS selectors for finding elements, chaining, promise-like AJAX) shaped the standard APIs we use today. It is still found on a large share of existing websites, in CMS themes and in internal tools, so reading, maintaining and eventually replacing jQuery code remains an everyday skill. This chapter covers the core API, events, effects and AJAX, and then plugins, coexistence with other code and migration to modern vanilla JavaScript.
<a id="81-jquery-core"></a>
8.1 jQuery Core
This subchapter covers why jQuery exists, the central $() function and the wrapped sets it returns, and the core methods for selecting, traversing and modifying the document.
Motivation and Cross-Browser Abstraction
To understand jQuery you have to picture the web of 2006. Internet Explorer 6 held most of the browser market, Firefox was growing, and Safari and Opera each had their own quirks. The DOM standards existed, but every browser implemented a different subset of them, with different bugs.
| Task | Standards browsers (2006) | Internet Explorer 6-8 |
|---|---|---|
| Register an event listener | el.addEventListener('click', fn, false) | el.attachEvent('onclick', fn) |
| Get the event object | Passed as the first argument | Global window.event |
| Get the event target | event.target | event.srcElement |
| Stop the default action | event.preventDefault() | event.returnValue = false |
| Create an AJAX request | new XMLHttpRequest() | new ActiveXObject('Microsoft.XMLHTTP') |
| Select by CSS selector | Not available (querySelectorAll arrived in 2008-2009) | Not available |
| Set element opacity | style.opacity = 0.5 | style.filter = 'alpha(opacity=50)' |
Every non-trivial script therefore began with a layer of "if this browser, do this, otherwise do that" code. Teams copied these helpers from blog posts, and each copy had its own bugs.
jQuery's answer. John Resig presented jQuery in January 2006 and released version 1.0 in August 2006. Its motto, "write less, do more", summarised three design decisions:
- Find elements with CSS selectors. Developers already knew CSS, so
$('#menu li.active')was easier to learn than chains ofgetElementsByTagNameloops. - Act on sets, not single elements. Every operation applies to all matched elements, and an empty set simply does nothing instead of throwing.
- Hide browser differences behind one API. One
.on()for events, one.css()for styles, one$.ajax()for requests, regardless of the browser underneath.
Analogy: jQuery is a universal travel adapter. The sockets (browsers) in each country are different, but you plug your device (your code) into the adapter once, and the adapter deals with the socket.
Example: the same task with and without jQuery (2006 style)
// Without jQuery: hide every element with class "note" when a button is clicked
function hideNotes() {
var all = document.getElementsByTagName('*'); // no getElementsByClassName in IE6
for (var i = 0; i < all.length; i++) {
if ((' ' + all[i].className + ' ').indexOf(' note ') !== -1) {
all[i].style.display = 'none';
}
}
}
var btn = document.getElementById('hide');
if (btn.addEventListener) {
btn.addEventListener('click', hideNotes, false); // standards browsers
} else {
btn.attachEvent('onclick', hideNotes); // old Internet Explorer
}
// With jQuery: the same behaviour in one line, in every supported browser
$('#hide').on('click', function () { $('.note').hide(); });
What jQuery contains.
+---------------------------------------------------------------+
| jQuery (core) |
+--------------+--------------+--------------+------------------+
| Selector | Traversal & | Events | Effects |
| engine | manipulation | (.on/.off, | (.fadeIn, |
| (Sizzle, | (.find, | delegation, | .animate, |
| CSS + extras)| .append ...) | normalized | queues) |
| | | event object)| |
+--------------+--------------+--------------+------------------+
| AJAX ($.ajax, shorthands) | Deferred / Promise | Utilities |
| | ($.Deferred, when) | ($.each ...)|
+-----------------------------+--------------------+-------------+
^ the "slim" build omits AJAX and effects (and, in jQuery 4,
Deferred) for a smaller download
Why it still matters. The browser problems jQuery solved are largely gone: querySelectorAll, classList, addEventListener, fetch and promises work the same in every current browser (Chapters 5 and 6). Yet jQuery still runs on a very large share of websites, mostly through WordPress and other CMS themes, Bootstrap 3 and 4 sites, and older line-of-business applications. Its major versions also track the browsers it supports:
| Version | Year | Notable change |
|---|---|---|
| 1.x | 2006-2016 | Supports Internet Explorer 6-8; .on()/.off() added in 1.7 |
| 2.x | 2013-2016 | Drops IE 6-8 for a smaller, faster core |
| 3.x | 2016 onwards | Promises/A+ compatible Deferreds, requestAnimationFrame animations, removal of long-deprecated APIs |
| 4.x | 2026 | Drops IE 10 and older, removes many deprecated utilities ($.trim, $.isArray ...), source written as ES modules |
Many of jQuery's ideas became standards: document.querySelectorAll follows the selector-based approach, classList mirrors .addClass()/.removeClass(), and fetch with promises replaced the callback style of early $.ajax.
Key Takeaways
- jQuery was created to hide large incompatibilities between browsers in DOM selection, events, styles and AJAX.
- Its three core ideas are CSS selectors for finding elements, operations on whole sets of elements, and one consistent API over all browsers.
- Modern browsers have standardised most of what jQuery provided, but much existing code still depends on it.
- jQuery versions are tied to browser support: 1.x for old IE, 2.x and 3.x for modern browsers, 4.x dropping IE 10 and older.
Practice
- (Beginner) Rewrite the "Without jQuery" example using only modern standard APIs (
querySelectorAll,addEventListener,forEach). Compare its length with the jQuery version.- (Intermediate) Open the developer tools on three large websites you use and type
window.jQuery && jQuery.fn.jqueryin the console. Record which sites load jQuery and which version, and suggest why each might still use it.- (Interview) "Why would a team still choose jQuery in a new project today, and why would another team refuse to?" Hint: weigh existing plugins, team skills and CMS integration against bundle size, duplicated standard APIs and the component model of modern frameworks.
The jQuery Function and Wrapped Sets
Almost everything in jQuery starts with one function, jQuery, usually written with its alias $. What you get back is not a DOM element but a jQuery object, also called a wrapped set: an array-like container holding zero or more DOM elements and offering all of jQuery's methods.
Analogy: a wrapped set is a shopping basket. You fill it with items (elements), and then a single instruction ("gift-wrap everything in the basket") applies to every item. An empty basket is still a valid basket, and the instruction just does nothing.
The many faces of $(). The function behaves differently depending on its argument:
| Call | What it does | Example |
|---|---|---|
$(selector) | Finds matching elements in the document | $('ul.todo > li') |
$(selector, context) | Finds matches inside a context element or set | $('input', '#signup') |
$(htmlString) | Creates new elements from HTML (not yet in the document) | $('<li class="item">New</li>') |
$(htmlString, props) | Creates one element and sets attributes, events and content | $('<a>', { href: '/x', text: 'Go' }) |
$(element) or $(nodeList) | Wraps existing DOM elements | $(this), $(document.body) |
$(jQueryObject) | Makes a copy of the set | $($items) |
$(function) | Runs the function when the DOM is ready (8.2) | $(function () { ... }) |
Inside a wrapped set. A jQuery object has numeric indices and a length property, like an array, but its prototype is jQuery.fn (an alias of jQuery.prototype), not Array.prototype.
$('li') --> jQuery object
+--------+--------+--------+--------+---------------------+
| 0 | 1 | 2 | length | [[Prototype]] |
| <li> | <li> | <li> | 3 | jQuery.fn |
+--------+--------+--------+--------+ (.css .on .find .. |
^ raw DOM elements .each .eq .get ...)|
+------------------+
Example: working with wrapped sets
const $items = $('#todo li'); // convention: prefix variables holding jQuery objects with $
console.log($items.length); // number of matched elements
console.log($items.jquery); // version string, e.g. "3.7.1": proof it is a jQuery object
// Getting raw DOM elements back out
const firstEl = $items[0]; // DOM element (undefined if the set is empty)
const secondEl = $items.get(1); // same as $items[1]
const lastEl = $items.get(-1); // negative index counts from the end
const array = $items.toArray(); // real Array of DOM elements
// Getting a smaller jQuery object instead (keeps the jQuery methods)
const $first = $items.eq(0); // jQuery object with one element (or none)
// Implicit iteration: one call, applied to every element
$items.addClass('pending');
// Explicit iteration when each element needs different treatment
$items.each(function (index, element) {
// inside .each, `this` === element (a raw DOM element, not a jQuery object)
$(element).attr('data-position', index + 1);
});
// Creating elements
const $newItem = $('<li>', {
text: 'Buy milk', // sets textContent safely (no HTML parsing)
class: 'item', // quoted 'class' was required in old engines; fine either way today
'data-id': 42,
on: { click: () => console.log('clicked') } // attaches an event handler
});
$('#todo').append($newItem);
Empty sets do not throw. $('#does-not-exist').hide() silently does nothing. This is convenient, but it also hides typos. Check length when you expect a match:
const $form = $('#sigup'); // typo: no element matches
if ($form.length === 0) {
console.warn('Signup form not found'); // without this check the bug stays invisible
}
Getters vs setters. Most methods are overloaded: called without a value they read, with a value they write.
| Kind | Applies to | Returns | Example |
|---|---|---|---|
| Getter | First element only | The value | $('input').val() reads only the first input |
| Setter | Every element | The jQuery object (chainable) | $('input').val('') clears all inputs |
$ vs jQuery. Both names point to the same function. $ is only a short alias, which matters when another library also wants $ (see noConflict and Coexistence, 8.3). Static helpers live on the function itself ($.each, $.ajax, $.extend), while methods for wrapped sets live on $.fn.
Key Takeaways
$()selects, creates or wraps elements, or registers a ready handler, depending on its argument.- A jQuery object is an array-like wrapper of DOM elements whose prototype is
jQuery.fn. - Methods apply to every element in the set (implicit iteration), and an empty set causes no error.
- Getters read from the first element only, setters write to all elements and return the set.
- Use
[i]or.get(i)to get a DOM element,.eq(i)to get a one-element jQuery object.
Practice
- (Beginner) On a page with a list of five items, log the text of each item using
.each(), then log the text of only the third item using.eq()and again using.get(). Explain the difference between the two values.- (Intermediate) Build a table row with
$('<tr>')and three cells created with the$(html, props)form, where the last cell holds a "Delete" button with a click handler that removes the row. Append ten such rows in a loop.- (Interview) "Why does
$('p').text()return the text of all paragraphs joined together, while$('p').html()returns only the first paragraph's HTML?" Hint: check the documented getter behaviour of each method;.text()is a deliberate exception to the "first element only" rule.
Selectors and Filters
Selecting elements is the first step of nearly every jQuery operation. jQuery accepts any CSS selector, plus a set of jQuery-only extensions that were added before CSS supported similar features.
How selection works. When you call $('...'), jQuery first tries fast paths (a plain #id, tag or class), then the browser's native querySelectorAll. Only when the selector contains a jQuery-only extension does it fall back to its own selector engine, Sizzle, which parses and evaluates the selector in JavaScript.
$('selector')
|
v
simple #id / tag / .class? --yes--> getElementById / getElementsBy... (fastest)
| no
v
valid CSS for querySelectorAll? --yes--> native querySelectorAll (fast)
| no (uses :visible, :eq(), :contains() ...)
v
Sizzle: parse and match in JavaScript (slower)
Standard CSS selectors work exactly as in stylesheets (Chapter 2):
$('#main') // by id
$('.card') // by class
$('nav a[href^="https"]') // descendant + attribute prefix match
$('ul > li:first-child') // child combinator + structural pseudo-class
$('input[type="checkbox"]:checked') // checked checkboxes
$('h2 + p') // paragraph immediately after an h2
$('li:not(.done)') // negation
jQuery extensions. These are not valid CSS. They still work, but they force the slower Sizzle path, and several are deprecated.
| Extension | Meaning | Preferred alternative |
|---|---|---|
:visible / :hidden | Element takes up layout space or not | .filter(':visible') after a CSS selection, or check offsetParent / getClientRects() |
:input | input, textarea, select and button | $('input, textarea, select, button') |
:checkbox, :radio, :text ... | Input of a given type | input[type="checkbox"] |
:contains(text) | Element whose text includes text | .filter((i, el) => el.textContent.includes(text)) |
:header | h1 to h6 | $('h1, h2, h3, h4, h5, h6') |
:animated | Currently animated by jQuery | none in CSS |
:first, :last, :eq(n), :even, :odd, :gt(n), :lt(n) | Position within the matched set (deprecated since 3.4) | .first(), .last(), .eq(n), .filter(':even'), .slice() |
The positional trap. :first and :first-child look similar but mean different things.
<ul id="a"><li>A1</li><li>A2</li></ul>
<ul id="b"><li>B1</li><li>B2</li></ul>
$('li:first-child') // A1 and B1: every li that is the first child of its parent (CSS)
$('li:first') // A1 only: the first element of the whole matched set (jQuery, deprecated)
$('li').first() // A1 only: same result, using the recommended method
$('li:eq(2)') // B1: index 2 in the matched set (0-based)
$('li:nth-child(2)') // A2 and B2: second child of each parent (1-based, CSS)
Filtering methods. Instead of extensions, select with plain CSS and then narrow the set with methods. This is both faster and clearer.
| Method | Keeps | Example |
|---|---|---|
.filter(selector or fn) | Elements that match | $('tr').filter(':even') |
.not(selector or fn) | Elements that do not match | $('input').not('[disabled]') |
.has(selector) | Elements that contain a match | $('li').has('ul') |
.first(), .last(), .eq(i) | One element by position | $('li').eq(-1) |
.slice(start, end) | A range | $('li').slice(0, 3) |
.is(selector) | Returns true/false, does not filter | $(this).is(':checked') |
// Highlight rows whose price cell is above 100
$('#products tbody tr')
.filter(function () {
const price = parseFloat($(this).find('.price').text()); // `this` is the current <tr>
return price > 100; // keep the row when true
})
.addClass('expensive');
Scope your selections. Searching the whole document repeatedly is the most common jQuery performance problem. Pass a context or start from a cached element:
// Slow pattern: three full-document searches
$('#cart .item').addClass('x');
$('#cart .total').text(total);
$('#cart .empty-msg').hide();
// Better: find the container once, then search inside it
const $cart = $('#cart');
$cart.find('.item').addClass('x');
$cart.find('.total').text(total);
$cart.find('.empty-msg').hide();
Escaping special characters. An id such as user.name or item:3 contains characters that have a meaning in selectors. Escape them with $.escapeSelector (3.0+) or the standard CSS.escape:
$('#' + $.escapeSelector('item:3')) // selects id="item:3"
Key Takeaways
- jQuery accepts all CSS selectors and uses the native engine for them. jQuery-only extensions fall back to the slower Sizzle engine.
- Positional pseudo-classes such as
:first,:eq()and:evenrefer to the matched set, not to the DOM tree, and are deprecated in favour of methods. - Prefer "select with CSS, then refine with
.filter(),.not(),.has(),.eq()". - Cache and scope selections (
$container.find(...)) instead of repeatedly searching the whole document. - Escape ids and classes containing selector characters with
$.escapeSelector.
Practice
- (Beginner) Given a table of students with name, age and grade columns, select the rows where the grade cell contains "A" in two ways: with
:contains()and with.filter(). Confirm both return the samelength.- (Intermediate) Rewrite each of the following without jQuery extensions:
$('tr:odd'),$(':input:enabled'),$('div:has(> img):visible'),$('li:gt(4)'). Explain which of them can be passed directly todocument.querySelectorAll.- (Interview) "A page calls
$('.row:visible')inside a scroll handler and scrolling stutters. Why?" Hint: consider which engine evaluates:visible, what it must compute for every candidate element, and how often a scroll handler runs.
Traversing and Chaining
Once you have a set of elements, you often need related elements: the parent form of a button, the next sibling of a heading, every list item below a menu. Traversal methods move from the current set to a new set. Because nearly every method returns a jQuery object, calls can be chained into one readable pipeline.
Traversal methods by direction.
.parents() .closest(sel)
^ ^
| |
+----------------------+
| .parent() |
+----------------------+
.prev() .prevAll() | .next() .nextAll()
<------------------------ [ current ] ------------------------>
.siblings() = all of both sides
|
.children() .find(sel)
v
| Direction | Methods | Notes |
|---|---|---|
| Down | .children([sel]), .find(sel), .contents() | .children goes one level, .find searches all descendants; .contents includes text nodes |
| Up | .parent([sel]), .parents([sel]), .parentsUntil(sel), .closest(sel) | .closest starts at the element itself and returns at most one ancestor per element |
| Sideways | .next(), .prev(), .nextAll(), .prevAll(), .nextUntil(), .prevUntil(), .siblings() | Element nodes only, text nodes are skipped |
| Set operations | .add(sel), .addBack(), .end() | Combine or go back to earlier sets |
.closest() vs .parents(). These are often confused:
.closest(sel) | .parents(sel) | |
|---|---|---|
| Starts at | The element itself | The parent |
| Stops | At the first match | Never, collects all matching ancestors |
| Result size per element | 0 or 1 | 0 or more |
| Typical use | "Which card or row am I in?" | "All the nested lists I am inside" |
How chaining works. Each traversal method pushes a new set and remembers the previous one, so .end() can return to it. Manipulation methods return the same set they were called on.
$('#faq') // set 1: the FAQ container
.find('dt') // set 2: all questions (remembers set 1)
.addClass('question') // still set 2
.first() // set 3: first question
.addClass('open') // still set 3
.end() // back to set 2
.next('dd') // set 4: the answer after each question
.hide() // hide every answer
.end() // back to set 2
.end() // back to set 1
.attr('data-ready', 'true'); // applied to the container
Indentation that follows the set changes, as above, keeps long chains readable. Very long chains with several .end() calls are hard to follow, so splitting them using named variables is usually better.
Example: a common traversal pattern
<table id="orders">
<tr data-id="17"><td>Keyboard</td><td><button class="remove">Remove</button></td></tr>
<tr data-id="18"><td>Mouse</td><td><button class="remove">Remove</button></td></tr>
</table>
$('#orders').on('click', '.remove', function () {
const $row = $(this).closest('tr'); // from the button up to its row
const id = $row.data('id'); // 17 or 18 (converted to a number)
const name = $row.children().first().text(); // first cell's text
if (confirm(`Remove ${name}?`)) {
$row.fadeOut(200, function () { $(this).remove(); }); // remove after the animation
}
});
Where the chain breaks. Getters return values, not jQuery objects, so they end a chain:
$('input').val().addClass('x'); // TypeError: .val() returned a string
$('input').addClass('x').val(); // fine: getter last
Key Takeaways
- Traversal methods move down (
.find,.children), up (.parent,.closest,.parents) and sideways (.next,.prev,.siblings), always returning a new jQuery object. .closest()includes the element itself and returns at most one ancestor, which makes it the standard way to find the "owning" container of a clicked element.- Chaining works because methods return jQuery objects.
.end()returns to the previous set,.addBack()merges it with the current one. - Getters return plain values and therefore end a chain.
Practice
- (Beginner) In a nested list menu, write code that, given any clicked
<a>, highlights its<li>, all ancestor<li>elements and the top-level<ul>. Use.parents()and.closest().- (Intermediate) Write one chain that starts from
$('h2'), adds the classsection-title, selects all following elements up to (but not including) the nexth2, wraps them in<div class="section-body">, then returns to the headings and makes them clickable toggles for their section.- (Interview) "What is the difference between
$(el).find('.x')and$('.x', el), and between.children('.x')and.find('> .x')?" Hint: look at how jQuery implements the context argument, and think about the depth each method searches.
DOM Manipulation Methods
jQuery's manipulation methods insert, move, replace, copy and remove content. They accept HTML strings, DOM elements or jQuery objects, and they work on whole sets, which makes structural changes short to write.
Insertion methods. The methods come in pairs that do the same thing with the subject and target swapped:
| Position | "Target.method(content)" | "Content.methodTo(target)" |
|---|---|---|
| Inside, at the end | $target.append(content) | $content.appendTo(target) |
| Inside, at the start | $target.prepend(content) | $content.prependTo(target) |
| Outside, before | $target.before(content) | $content.insertBefore(target) |
| Outside, after | $target.after(content) | $content.insertAfter(target) |
.before()
+-------------- <ul id="list"> --------------+
| .prepend() |
| <li>Existing 1</li> |
| <li>Existing 2</li> |
| .append() |
+--------------------------------------------+
.after()
The pairs differ in what they return: .append() returns the target, .appendTo() returns the inserted content. Pick the one that leaves the chain on the set you want to keep working with.
$('#list').append('<li>Last</li>').addClass('updated'); // chain continues on #list
$('<li>First</li>').prependTo('#list').addClass('highlight'); // chain continues on the new <li>
Move vs copy. Inserting an element that is already in the document moves it. If the target set has several elements, jQuery clones the content for every target except the last:
$('.card').append($('#badge')); // #badge is cloned into every card but the last, where the original moves
Content methods.
| Method | Reads/writes | Safe for user input? |
|---|---|---|
.text(str) | Text content, HTML is escaped | Yes |
.html(str) | Inner HTML, parsed as markup | No, use only with trusted markup |
.val(value) | Value of form controls (an array for <select multiple>; an array setter also checks matching checkboxes) | Yes |
const comment = '<img src=x onerror="alert(1)">'; // attacker-controlled input
$('#out').html(comment); // XSS: the onerror handler runs (see Chapter 6 on sanitization)
$('#out').text(comment); // safe: shown as literal text
Wrapping, replacing and removing.
| Method | Effect |
|---|---|
.wrap(html) / .wrapAll(html) / .wrapInner(html) | Wrap each element, all elements together, or each element's content |
.unwrap() | Remove the parent, keep the element |
.replaceWith(content) / .replaceAll(target) | Replace elements |
.empty() | Remove all children (and their jQuery data and events) |
.remove([sel]) | Remove elements and their jQuery data and event handlers |
.detach([sel]) | Remove elements but keep data and handlers for re-insertion |
.clone(withEvents, deepWithEvents) | Copy elements, optionally with handlers and data |
.remove() vs .detach() matters because jQuery stores handlers and data in an internal cache. .remove() cleans that cache (preventing memory leaks), while .detach() keeps it so that a detached element can be re-inserted fully working:
const $panel = $('#panel').detach(); // take it out, keep its click handlers
// ... rearrange the page ...
$panel.appendTo('#sidebar'); // handlers still work
// If you remove an element with native el.remove(), jQuery's cache entry for it is never
// cleaned in older versions: prefer jQuery's .remove() for elements managed by jQuery.
Performance: build once, insert once. Every insertion into the live document can trigger style and layout work (Chapter 6, reflow). Build content off-document and insert it in one operation:
const users = [/* 1,000 objects */];
// Slow: 1,000 separate insertions into the live DOM
users.forEach(u => $('#users').append($('<li>').text(u.name)));
// Fast: build detached elements, then a single append
const $items = users.map(u => $('<li>').text(u.name)[0]); // array of DOM elements
$('#users').append($items); // one insertion
Key Takeaways
- Insertion methods come in pairs (
append/appendTo,before/insertBefore...) that differ only in subject and return value. - Inserting an existing element moves it, and multiple targets receive clones.
.text()escapes content,.html()parses it: never pass untrusted input to.html(),.append()or$()as an HTML string..remove()deletes elements together with jQuery data and handlers,.detach()keeps them for re-insertion.- Build content off-document and insert it once to avoid repeated reflows.
Practice
- (Beginner) Given an array of strings, render them as a numbered list inside
#output, then add a "Clear" button that empties the list.- (Intermediate) Implement "move up" and "move down" buttons for list items using
.prev(),.next(),.before()and.after(). Make sure the click handlers still work after the items have moved, and explain why they do.- (Interview) "A dashboard re-renders a widget every 5 seconds with
$('#widget').html(newMarkup), and memory grows steadily. What could be leaking and how would you fix it?" Hint: think about handlers and data attached to the old children, plugins that hold references, and how.html()and.empty()clean up.
CSS, Attribute and Data Methods
Beyond structure, you constantly need to change how elements look (styles and classes), what they describe (attributes and properties) and what extra information they carry (data). jQuery offers one family of methods for each, all following the getter/setter pattern.
Styles with .css().
const $box = $('#box');
$box.css('color'); // getter: computed value of the first element, e.g. "rgb(0, 0, 0)"
$box.css(['width', 'height']); // getter: { width: "200px", height: "100px" }
$box.css('background-color', 'gold'); // setter: CSS name or camelCase both work
$box.css({ width: 300, opacity: 0.5 }); // object form; numbers get "px" where the property needs a unit
$box.css('width', '+=50'); // relative change: 50px wider than now
$box.css('margin-left', function (i, current) {
return parseFloat(current) * 2; // function form: compute from the current value
});
$box.css('color', ''); // empty string removes the inline style
.css() as a getter returns the computed style (like getComputedStyle, Chapter 6), while as a setter it writes inline styles. Inline styles beat most stylesheet rules and make theming harder, so prefer classes for anything beyond one-off dynamic values.
Classes.
| Method | Effect |
|---|---|
.addClass('a b') | Add one or more classes |
.removeClass('a') / .removeClass() | Remove some or all classes |
.toggleClass('a', [state]) | Toggle, or force on/off with a boolean |
.hasClass('a') | true if any element in the set has the class |
All of them also accept a function that returns the class names to use, computed per element.
Attributes vs properties. This distinction (covered in Chapter 6) caused so many bugs that jQuery 1.6 split one method into two:
- An attribute is what is written in the HTML source (
<input checked>). It reflects the initial state. - A property is the live value on the DOM object (
input.checked). It reflects the current state.
Use .prop() for | Use .attr() for |
|---|---|
checked, selected, disabled, readOnly | href, src, alt, title, role, aria-* |
selectedIndex, tagName, nodeName | custom attributes that must appear in the markup |
| any live state the user can change | values you want to read as written in the HTML |
<input type="checkbox" id="agree" checked>
// User unticks the box...
$('#agree').attr('checked'); // "checked": the attribute from the HTML has not changed
$('#agree').prop('checked'); // false: the current state
$('#agree').is(':checked'); // false: also reads the live state
$('#submit').prop('disabled', true); // correct way to disable a control
$('a.ext').attr({ target: '_blank', rel: 'noopener' }); // set several attributes
$('img').removeAttr('title');
Data with .data(). HTML5 data-* attributes let markup carry custom values (Chapter 6, dataset). jQuery's .data() reads them, but with two important differences from dataset:
- Type conversion.
"42"becomes the number42,"true"becomestrue,"null"becomesnull, and strings starting with{or[are parsed as JSON. A numeric string is converted only if converting it back gives the same string, so"9.90"and"007"stay strings. - A separate cache. The attribute is read once, on first access. Writes with
.data(key, value)go to jQuery's internal cache only, not to the attribute.
<div id="product" data-id="42" data-price="9.90" data-tags='["new","sale"]' data-in-stock="true"></div>
const $p = $('#product');
$p.data('id'); // 42 (number)
$p.data('tags'); // ["new", "sale"] (array parsed from JSON)
$p.data('inStock'); // true: data-in-stock is read with a camelCase key
$p.data('price'); // "9.90" (string: 9.9 would not round-trip to "9.90")
$p.data(); // { id: 42, price: "9.90", tags: [...], inStock: true }
$p.data('id', 99); // updates jQuery's cache only
$p.data('id'); // 99
$p.attr('data-id'); // "42": the attribute is unchanged, so CSS [data-id="99"] will not match
$p.data('chart', chartInstance); // .data() can store any object, e.g. a plugin instance
Use .attr('data-x', v) when CSS selectors or other scripts need to see the change, and .data() for private values (including objects) attached to an element.
Dimensions and position.
| Method | Measures |
|---|---|
.width() / .height() | Content box |
.innerWidth() / .innerHeight() | Content + padding |
.outerWidth() / .outerHeight() | Content + padding + border |
.outerWidth(true) | ... + margin |
.offset() | { top, left } relative to the document (also a setter) |
.position() | { top, left } relative to the offset parent |
.scrollTop() / .scrollLeft() | Scroll position (also setters) |
These are getters that force layout, so reading them inside loops that also write styles causes layout thrashing (Chapter 6).
Key Takeaways
.css()reads computed styles and writes inline styles. Prefer.addClass()/.toggleClass()for state-driven styling.- Use
.prop()for live state such ascheckedanddisabled,.attr()for HTML attributes as written. .data()converts types and stores values in a private cache. It does not updatedata-*attributes.- Dimension methods (
.width,.outerHeight,.offset) measure different boxes and force layout when read.
Practice
- (Beginner) Build a "dark mode" toggle that uses
.toggleClass()on<body>and stores the choice inlocalStorage. Do not use.css().- (Intermediate) Create a "select all" checkbox that checks or unchecks every checkbox in a table, and that shows an indeterminate state when only some rows are checked. Use
.prop()throughout and explain why.attr()would fail after the user clicks a few boxes.- (Interview) "A colleague sets
$(el).data('state', 'open')and then styles it with[data-state="open"] { ... }, but the style never applies. Explain the bug." Hint: where does.data()write, and what does a CSS attribute selector read?
<a id="82-events-effects-and-ajax"></a>
8.2 Events, Effects and AJAX
This subchapter covers jQuery's event system, page lifecycle hooks, animations and their queues, and the AJAX and Deferred APIs used for asynchronous communication with the server.
Event Binding with on and off
The browser's event model (Chapter 6) is powerful but was historically inconsistent. jQuery wraps it in one pair of methods, .on() and .off(), that work the same everywhere and add features the native API lacked for a long time: removing groups of handlers by name, passing data to handlers and triggering events from code.
The .on() signature.
$elements.on( events [, selector] [, data], handler )
| | | |
| | | +-- function(event, ...extraArgs)
| | +------------ any value, available as event.data
| +------------------------- present => delegated handler (next topic)
+------------------------------------ "click", "click keyup", "click.menu" ...
// Basic binding
$('#save').on('click', function (event) {
// `this` is the DOM element the handler is bound to (event.currentTarget)
$(this).prop('disabled', true);
});
// Several events, one handler
$('#search').on('input change', function () { runSearch(this.value); });
// Several events, different handlers (object form)
$('.tile').on({
mouseenter() { $(this).addClass('hover'); },
mouseleave() { $(this).removeClass('hover'); }
});
// Passing data to the handler
$('.qty').on('change', { max: 10 }, function (event) {
if (+this.value > event.data.max) this.value = event.data.max;
});
// Run once, then unbind automatically
$('#banner .close').one('click', function () { $('#banner').remove(); });
The normalised event object. jQuery passes its own jQuery.Event object, which copies common properties from the native event and fixes historical differences:
| Property / method | Meaning |
|---|---|
event.target | Element where the event started |
event.currentTarget | Element whose handler is running (same as this) |
event.delegateTarget | Element the handler was attached to (differs for delegated handlers) |
event.which | Normalised key or mouse button (legacy; prefer event.key and event.button) |
event.pageX, event.pageY | Pointer position relative to the document |
event.data | The data value passed to .on() |
event.namespace | The namespace used when the event was triggered |
event.originalEvent | The native browser event, for anything jQuery does not copy |
preventDefault(), stopPropagation(), stopImmediatePropagation() | As in the DOM, plus isDefaultPrevented() etc. |
Properties that jQuery does not copy, such as dataTransfer in drag-and-drop or touches in touch events, are read from event.originalEvent.
return false. Returning false from a jQuery handler calls both preventDefault() and stopPropagation(). This differs from addEventListener, where the return value is ignored (Chapter 6). Calling the method you actually mean is clearer:
$('a.ajax').on('click', function (e) {
e.preventDefault(); // stop navigation, but let other code see the click bubble
loadInto('#content', this.href);
});
Removing handlers with .off(). To remove a specific handler you need a reference to the same function, or a namespace. A namespace is a label after a dot in the event name. It lets a plugin or module remove only its own handlers without touching anyone else's.
function onResize() { /* ... */ }
$(window).on('resize.gallery', onResize); // namespaced handler
$(window).on('scroll.gallery', updateArrows);
$(window).on('resize', otherCodeHandler); // someone else's handler
$(window).off('resize', onResize); // remove one specific function
$(window).off('.gallery'); // remove ALL gallery handlers, any event type
$(window).off('resize.gallery'); // only the gallery's resize handlers
$('#form').off(); // remove every jQuery handler on #form (use with care)
Triggering events from code.
| Method | Bubbles | Runs default action | Elements | Returns |
|---|---|---|---|---|
.trigger(type, [extra]) | Yes | Yes (e.g. submit, focus), except that a triggered click on an <a> does not follow the link | All in set | jQuery object |
.triggerHandler(type, [extra]) | No | No | First only | Handler's return value |
// Custom events form a simple publish/subscribe mechanism between components
$('#cart').on('cart:updated', function (event, count, total) {
$('#cart-badge').text(count); // extra arguments arrive after the event object
});
$('#cart').trigger('cart:updated', [3, 59.97]);
Methods you will see in old code.
| Old API | Status | Modern equivalent |
|---|---|---|
.bind() / .unbind() | Deprecated (3.0) | .on() / .off() |
.delegate() / .undelegate() | Deprecated (3.0) | .on(events, selector, handler) |
.live() / .die() | Removed (1.9) | $(document).on(events, selector, handler) |
.click(fn), .submit(fn), .keyup(fn) ... | Deprecated (3.3) | .on('click', fn) ... |
.load(fn), .unload(fn), .error(fn) | Removed (3.0) | .on('load', fn) ... |
Limitations. jQuery calls addEventListener internally but does not expose its options object, so there is no way to request { passive: true } (important for smooth scrolling with touch and wheel handlers), capture or an AbortSignal. When you need those, use addEventListener directly.
Key Takeaways
.on(events, [selector], [data], handler)is the single binding method;.off()removes handlers,.one()binds a handler that runs once.- Handlers receive a normalised
jQuery.Event; the native event is inevent.originalEvent. - Returning
falsefrom a jQuery handler both prevents the default action and stops propagation. - Namespaces (
click.myModule) let code remove only its own handlers. .trigger()simulates an event with bubbling and default action;.triggerHandler()only runs the handlers.
Practice
- (Beginner) Add a character counter to a
<textarea>that updates on everyinputevent and turns red above 140 characters. Then remove the handler with.off()when a "Disable counter" button is clicked.- (Intermediate) Write a
tooltipmodule whoseinit()bindsmouseenter,mouseleaveandkeydownhandlers using the namespace.tooltip, and whosedestroy()removes all of them in one call without affecting other handlers on the same elements. Prove it by adding an unrelatedmouseenterhandler that must keep working.- (Interview) "What is the difference between
$('#f').trigger('submit')and$('#f')[0].submit()?" Hint: consider which one runs jQuery and nativesubmithandlers and whether validation in those handlers can stop the submission.
Delegated Events in jQuery
Binding a handler to every row of a table works until rows are added later: the new rows have no handlers. Event delegation (Chapter 6) solves this by binding one handler to a stable ancestor and checking, when the event bubbles up, whether it started inside a matching element. jQuery builds this directly into .on(): pass a selector as the second argument.
Analogy: instead of giving every employee their own receptionist, the building has one front desk. Every visitor passes through it, and the receptionist checks which office they are heading to.
Direct vs delegated binding.
// Direct: one handler per existing .delete button; buttons added later get nothing
$('#todo .delete').on('click', removeItem);
// Delegated: one handler on #todo, works for current AND future .delete buttons
$('#todo').on('click', '.delete', removeItem);
Click on <button class="delete"> inside the 3rd <li>
#todo <-- handler attached here, runs with this = the matched .delete button
|
<li> <-- event bubbles through (does it match ".delete"? no)
|
<button class="delete"> <-- event.target (matches ".delete": handler will run for it)
jQuery walks from event.target up to #todo and runs the handler once for every
element on that path that matches the selector.
What this and the event properties point to.
| Value | Refers to |
|---|---|
this / event.currentTarget | The element that matched the selector (the .delete button) |
event.target | The innermost element actually clicked (maybe an <svg> icon inside the button) |
event.delegateTarget | The element the handler is attached to (#todo) |
$('#todo').on('click', '.delete', function (event) {
// event.target might be the <i class="icon"> inside the button: do not rely on it
const $item = $(this).closest('li'); // `this` is always the .delete button
$item.remove();
$(event.delegateTarget).trigger('todo:changed'); // notify listeners on the list itself
});
// Items added later are handled automatically
$('#todo').append('<li>New task <button class="delete">x</button></li>');
What jQuery adds over manual delegation. Writing delegation by hand with event.target.closest(selector) is easy today, but jQuery's version also:
- Fixes events that do not bubble natively:
focusandblurare mapped tofocusin/focusout, andmouseenter/mouseleaveare emulated withmouseover/mouseout, so all of them can be delegated. - Sets
thisto the matched element and runs the handler for each matching ancestor on the path, innermost first. - Runs delegated handlers before handlers bound directly on the delegate element.
Choosing the delegate element.
| Choice | Pros | Cons |
|---|---|---|
document | Works for anything, anywhere | Every click on the page tests the selector; long path; any stopPropagation() deeper in the page silently breaks it |
Nearest stable container (#todo) | Short path, few events to test, clear ownership | The container itself must exist when you bind |
The removed .live() method always delegated to document, which is why large old applications became slow. Attach to the closest container that exists for the lifetime of the content.
Pitfalls.
- Stopping propagation is too late. By the time a delegated handler runs, the event has already bubbled through every element between the target and the delegate.
stopPropagation()only stops it from going further up. - Selector cost. The selector is tested on each element along the path for every event. Keep it simple (
.delete,tr[data-id]) and avoid jQuery extensions such as:visible. - High-frequency events. Delegating
mousemoveorscroll-related events across a huge container means a selector check on every event. Bind directly to the few elements that need them instead.
Key Takeaways
- Pass a selector as the second argument of
.on()to delegate: one handler on an ancestor serves current and future descendants. - Inside a delegated handler,
thisis the matched element,event.targetthe actual origin andevent.delegateTargetthe element holding the handler. - jQuery makes non-bubbling events such as
focus,blur,mouseenterandmouseleavedelegatable. - Delegate to the nearest stable container rather than
document, and keep the selector simple.
Practice
- (Beginner) Build a to-do list with an "Add" input, where each item has "Done" and "Delete" buttons. Use exactly one delegated
.on()call per action on the<ul>.- (Intermediate) Put a
<span>icon inside each "Delete" button and logevent.target,thisandevent.delegateTargeton click. Then rewrite the same delegation without jQuery usingaddEventListenerandElement.closest, making sure clicks on the icon still work.- (Interview) "A delegated handler on
documentstopped firing after a third-party widget was added to the page. What might the widget be doing?" Hint: think about what the widget's own handlers might call on events passing through it.
Document Ready and Lifecycle
Scripts that touch the DOM must not run before the elements they need exist. jQuery's ready mechanism runs code as soon as the HTML has been parsed, without waiting for images and other resources. Understanding when it fires, relative to the browser's own lifecycle events (Chapter 6), avoids both "element not found" bugs and unnecessary waiting.
The page lifecycle.
time --->
HTML parsing ............................... done
| <script> (blocking) runs here |
| v
| DOMContentLoaded ---> jQuery ready handlers
| (scripts with defer/type=module
| have already run)
| images, iframes, fonts, stylesheets still loading .......... done
| v
| window "load"
| user leaves the page ............................................. "pagehide" / "beforeunload"
Registering a ready handler.
// Recommended form (the only one not deprecated in jQuery 3)
$(function () {
$('#app').addClass('ready');
});
// Older, deprecated forms you will see in existing code
$(document).ready(function () { /* ... */ });
jQuery(document).ready(function ($) { /* ... */ });
// Removed in jQuery 3: never fires
$(document).on('ready', function () { /* ... */ });
The handler receives the jQuery function as its first argument, which allows a safe local $ even when $ belongs to another library (8.3):
jQuery(function ($) {
// inside here, $ is jQuery regardless of what the global $ is
});
Behaviour you should know.
| Behaviour | Detail |
|---|---|
| Late registration | If the DOM is already ready, the handler still runs (unlike a DOMContentLoaded listener added too late) |
| Asynchronous since 3.0 | Handlers always run asynchronously, even when the DOM is already ready, so code after $(fn) runs first |
| Order | Multiple handlers run in registration order |
| Errors | In 3.x an exception in one ready handler no longer prevents the others from running |
| Promise form | $.ready is a thenable: $.when($.ready, $.getJSON('/config')).then(start) |
$(function () { console.log('B: ready handler'); });
console.log('A: after registration');
// Output in jQuery 3: "A: after registration", then "B: ready handler"
Ready vs load. Use ready for anything that needs elements. Use load only when you need resources to be fully loaded, such as measuring image dimensions:
$(window).on('load', function () {
// all images are loaded: their natural sizes are known
const heights = $('.gallery img').map((i, img) => img.naturalHeight).get();
$('.gallery').css('min-height', Math.max(...heights));
});
Registering a load handler after the event has fired does nothing, so it must be registered early (or check document.readyState === 'complete' first).
Do you need ready at all? In modern pages, often not. A script placed at the end of <body>, or loaded with defer or type="module", already runs after the document is parsed:
<!-- In <head>: defer runs after parsing, before DOMContentLoaded, in document order -->
<script src="jquery.min.js" defer></script>
<script src="app.js" defer></script> <!-- safe to query the DOM immediately -->
Keeping $(fn) is harmless and makes the code safe if someone later moves the script tag.
Leaving the page. For saving state or sending analytics when the user leaves, listen to pagehide or visibilitychange. The unload event is unreliable on mobile, and browsers are phasing it out because it prevents the back/forward cache from working:
$(window).on('pagehide', function () {
navigator.sendBeacon('/analytics', JSON.stringify(collectStats()));
});
Key Takeaways
$(function () { ... })runs code once the DOM is parsed, without waiting for images. It is the only non-deprecated ready syntax in jQuery 3.- Ready handlers run even if registered late, and since jQuery 3 they always run asynchronously.
- Use
$(window).on('load', ...)only when you need all resources, and register it early. - Scripts with
defer,type="module"or at the end of<body>can access the DOM without a ready handler. - Use
pagehideorvisibilitychange, notunload, for end-of-session work.
Practice
- (Beginner) Create a page with a large image and log timestamps (
performance.now()) from a ready handler, aDOMContentLoadedlistener and aloadlistener. Explain the order you observe.- (Intermediate) Use
$.when($.ready, $.getJSON('config.json'))to start an application only when both the DOM and its configuration are available. Handle a failed configuration request by showing an error message in the page.- (Interview) "After upgrading from jQuery 1.12 to 3.x, a script that calls
$(init)and then immediately uses a variable set insideinitstarted failing. Why?" Hint: compare when ready handlers run in the two versions if the DOM is already ready.
Effects, Animation and Queues
Before CSS transitions were widely supported, jQuery's effects were the standard way to make interfaces feel smooth: panels sliding open, messages fading out. They animate CSS properties frame by frame in JavaScript, and they use a queue per element so that animations run one after another instead of colliding.
Built-in effects.
| Group | Methods | Animates |
|---|---|---|
| Show/hide | .show(), .hide(), .toggle() | Instant display change; with a duration also width, height and opacity |
| Fading | .fadeIn(), .fadeOut(), .fadeToggle(), .fadeTo(d, opacity) | Opacity, then display |
| Sliding | .slideDown(), .slideUp(), .slideToggle() | Height (and vertical padding/margin), then display |
| Custom | .animate(properties, [duration], [easing], [complete]) | Any numeric CSS property |
Durations are in milliseconds or the strings 'fast' (200 ms) and 'slow' (600 ms); the default is 400 ms. Built-in easings are 'swing' (default) and 'linear'; others come from jQuery UI or an easing plugin.
$('#notice').fadeIn(300); // fade in over 300 ms
$('#details').slideToggle('fast'); // open or close a panel
$('#msg').fadeOut(function () { $(this).remove(); }); // callback runs per element when done
$('#box').animate(
{ left: '+=200', opacity: 0.5, height: 'toggle' }, // relative values and 'toggle' are allowed
{ duration: 800, easing: 'linear', complete: () => console.log('done') }
);
.animate() handles only numeric values. Colours need the jQuery Color plugin or jQuery UI, and properties such as transform are not animated at all.
How animation works. jQuery computes a new value for each property on every frame (using requestAnimationFrame since 3.0) and writes it as an inline style:
frame: 0ms 16ms 33ms 50ms ... 400ms
opacity: 0.00 --> 0.02 --> 0.08 --> 0.17 ... 1.00 (swing easing)
^ each value written to element.style.opacity by JavaScript
The effects queue. Each element has a queue named "fx". Every effect method adds an entry, and entries run one at a time:
$('#card')
.slideDown(300) // queued: runs first
.delay(1000) // queued: wait 1 s (only delays queued effects, not .css() or .addClass())
.fadeTo(300, 0.4) // queued: runs after the delay
.queue(function (next) {
$(this).addClass('dimmed'); // non-animation step inside the queue
next(); // must be called or the queue stalls
})
.fadeTo(300, 1);
#card fx queue: [slideDown 300] -> [delay 1000] -> [fadeTo 0.4] -> [addClass] -> [fadeTo 1]
running waiting waiting waiting waiting
The queue build-up problem. A classic bug: hovering in and out of a menu quickly queues dozens of animations, which keep playing long after the mouse has left.
// Buggy: every hover adds two more animations to the queue
$('.menu').on('mouseenter', function () { $(this).find('.sub').slideDown(); })
.on('mouseleave', function () { $(this).find('.sub').slideUp(); });
// Fixed: stop the current animation and clear the queue before starting the next one
$('.menu').on('mouseenter', function () { $(this).find('.sub').stop(true).slideDown(); })
.on('mouseleave', function () { $(this).find('.sub').stop(true).slideUp(); });
| Method | Effect on the current animation | Effect on queued animations |
|---|---|---|
.stop() | Stops it where it is | Next one starts |
.stop(true) | Stops it where it is | Queue cleared |
.stop(true, true) | Jumps to its end state | Queue cleared |
.finish() | Jumps to its end state | All queued ones also jump to their end state |
Knowing when animations finish. .promise() returns a promise that resolves when all queued animations on all elements in the set are done:
$('.card').fadeOut(400).promise().done(function () {
// runs once, after every card has faded out
$('#empty-state').show();
});
Modern alternatives. jQuery animations run on the main thread and usually animate layout properties (height, left), which forces layout on every frame. CSS transitions and the Web Animations API (Chapter 6) can animate transform and opacity on the compositor, which stays smooth even when JavaScript is busy:
// jQuery: animates height from JavaScript on every frame
$('#panel').slideDown(300);
// Modern: toggle a class, let CSS animate transform/opacity
$('#panel').addClass('open'); // .panel { transition: transform .3s, opacity .3s; } .panel.open { ... }
Respect users who ask for less motion: check matchMedia('(prefers-reduced-motion: reduce)').matches and set $.fx.off = true, which makes all jQuery effects jump to their end state immediately.
Key Takeaways
- jQuery provides show/hide, fade and slide effects plus
.animate()for any numeric CSS property. - Effects are added to a per-element
fxqueue and run in sequence;.delay()and.queue()work only within that queue. - Use
.stop(true)or.finish()to avoid animation build-up on repeated events. .promise()tells you when every animation in a set has finished.- For new code, prefer CSS transitions or the Web Animations API on
transformandopacity, and honourprefers-reduced-motion.
Practice
- (Beginner) Create an FAQ where clicking a question slides its answer open and closes any other open answer. Make sure rapid clicking does not cause queued animations.
- (Intermediate) Animate a notification "toast" that slides in, waits 3 seconds, fades out and is removed from the DOM, using only queued effects. Then implement the same behaviour with a CSS transition and a
transitionendlistener, and compare the two in the Performance panel.- (Interview) "Why does
$('#el').delay(1000).addClass('x')add the class immediately?" Hint: which methods use thefxqueue, and how could you put a non-animation step into it?
jQuery.ajax and Shorthand Methods
AJAX, loading data without reloading the page (Chapter 1 and 5.4), originally required browser-specific XMLHttpRequest code. $.ajax() wrapped it in one configurable function, and shorthand methods made common requests one-liners. Much existing code still talks to servers this way, and its semantics differ from fetch in ways that matter during maintenance.
$.ajax() and its main settings.
$.ajax({
url: '/api/orders',
method: 'POST', // 'type' in code older than 1.9
data: JSON.stringify({ item: 17, qty: 2 }),
contentType: 'application/json', // describes what we SEND
dataType: 'json', // what we EXPECT back: parsed automatically
headers: { 'X-CSRF-Token': csrfToken },
timeout: 5000, // ms; failing with textStatus "timeout"
xhrFields: { withCredentials: true } // send cookies on cross-origin requests (CORS)
})
.done(function (data, textStatus, jqXHR) {
console.log('Created order', data.id, jqXHR.status);
})
.fail(function (jqXHR, textStatus, errorThrown) {
// textStatus: "error", "timeout", "abort" or "parsererror"
console.error(textStatus, jqXHR.status, jqXHR.responseJSON?.message);
})
.always(function () {
$('#spinner').hide();
});
| Setting | Purpose | Default |
|---|---|---|
method | HTTP method | 'GET' |
data | Object (serialised with $.param), string, or FormData | none |
contentType | Content-Type of the request body | 'application/x-www-form-urlencoded; charset=UTF-8' |
dataType | Expected response: json, html, text, xml, script, jsonp | Guessed from the response Content-Type |
processData | Convert data objects to a query string | true (set false for FormData) |
cache | false appends _=timestamp to GET URLs to bypass caches | true (except script, jsonp) |
timeout | Abort after N ms | none |
beforeSend, success, error, complete | Callback options (older style) | none |
The data/contentType trap. Passing an object as data does not send JSON. jQuery URL-encodes it:
$.ajax({ url: '/api', method: 'POST', data: { a: 1, tags: ['x', 'y'] } });
// body: a=1&tags%5B%5D=x&tags%5B%5D=y (form-encoded, Content-Type x-www-form-urlencoded)
$.ajax({ url: '/api', method: 'POST', data: JSON.stringify({ a: 1 }), contentType: 'application/json' });
// body: {"a":1} (JSON)
const fd = new FormData($('#upload')[0]); // file upload
$.ajax({ url: '/upload', method: 'POST', data: fd, processData: false, contentType: false });
// contentType: false lets the browser set multipart/form-data with the boundary
Shorthand methods.
| Shorthand | Equivalent to |
|---|---|
$.get(url, [data], [success], [dataType]) | $.ajax({ url, data, success, dataType, method: 'GET' }) |
$.post(url, [data], [success], [dataType]) | Same with method: 'POST' (form-encoded body) |
$.getJSON(url, [data], [success]) | $.get with dataType: 'json' |
$.getScript(url, [success]) | Loads and executes a script |
$el.load(url [selector], [data], [complete]) | GET HTML and insert it into the elements |
$.getJSON('/api/products', { category: 'books' }) // GET /api/products?category=books
.done(products => renderList(products));
// Load only the #news fragment of another page into the sidebar
$('#sidebar').load('/news.html #news', function (response, status) {
if (status === 'error') $(this).text('News unavailable');
});
// Serialise a form for submission
$('#contact').on('submit', function (e) {
e.preventDefault();
$.post(this.action, $(this).serialize()) // "name=Ann&email=ann%40example.com"
.done(() => $(this).replaceWith('<p>Thanks!</p>'));
});
The jqXHR object. Every AJAX method returns a jqXHR: a wrapper around the native XHR that also implements jQuery's promise interface (done, fail, always, then, catch). It exposes status, statusText, responseText, responseJSON, getResponseHeader() and abort().
When is a request a failure? This is the most important difference from fetch (Chapter 5):
| Situation | $.ajax | fetch |
|---|---|---|
| Network failure, CORS blocked | Fails | Rejects |
| HTTP 404 or 500 | Fails | Resolves (response.ok === false) |
dataType: 'json' but invalid JSON | Fails with "parsererror" | Resolves; response.json() rejects later |
| Timeout | Fails with "timeout" (setting) | Needs AbortSignal.timeout() |
Empty 200 body with dataType: 'json' | Fails with "parsererror" (a 204 No Content succeeds) | Resolves; response.json() rejects later |
Global AJAX events and setup. jQuery fires events on document for every request, which is handy for a global loading indicator:
$(document)
.on('ajaxStart', () => $('#spinner').show()) // first request starts
.on('ajaxStop', () => $('#spinner').hide()) // all requests finished
.on('ajaxError', (e, jqXHR, settings) => {
if (jqXHR.status === 401) location.href = '/login';
});
// Add a header to every same-origin request, without changing global defaults for all code
$.ajaxPrefilter(function (options, originalOptions, jqXHR) {
if (!options.crossDomain) jqXHR.setRequestHeader('X-CSRF-Token', csrfToken);
});
$.ajaxSetup() also changes defaults for every request, including those made by third-party plugins, so the jQuery documentation discourages it in favour of prefilters or explicit options.
Security notes. $.getScript, dataType: 'script' and jsonp execute code from the server, so use them only with trusted origins. Versions of jQuery before 3.0 could also execute a cross-domain response as a script when dataType was not set, a known vulnerability that is one of many reasons to upgrade (8.3).
Key Takeaways
$.ajax(settings)is the general method;$.get,$.post,$.getJSON,$.getScriptand.load()are shorthands.dataobjects are form-encoded by default. To send JSON,JSON.stringifythe body and setcontentType: 'application/json'.dataTypedeclares the expected response and triggers automatic parsing; invalid JSON becomes a"parsererror"failure.$.ajaxfails on HTTP error statuses, unlikefetch, which only rejects on network errors.- Global events (
ajaxStart,ajaxStop,ajaxError) and$.ajaxPrefiltercentralise loading indicators and headers.
Practice
- (Beginner) Use
$.getJSONto load a public JSON file (or a localusers.json) and render the entries in a table. Show a spinner during the request using the globalajaxStart/ajaxStopevents.- (Intermediate) Write a
postJSON(url, body)helper with$.ajaxthat sends JSON, sets a CSRF header, uses a 5-second timeout and maps failures to user-friendly messages for timeout, 4xx, 5xx and parse errors. Then write the same helper withfetchand list every difference in behaviour you had to handle explicitly.- (Interview) "A form posted with
$.post('/api/users', { name, roles })arrives at an Express server withreq.bodyempty. What is going on?" Hint: compare theContent-TypejQuery sends with the body parsers the server has installed (Chapter 12).
Deferred Objects and Promises
Asynchronous code needs a way to say "do this when that finishes". Before native promises existed (Chapter 5), jQuery 1.5 (2011) introduced Deferred objects, which are used throughout jQuery itself: every AJAX call, every .promise() on animations and $.ready return one. Knowing how Deferreds relate to native promises helps when you combine old and new code.
Deferred vs promise. A Deferred is the full controller: whoever holds it can resolve or reject it. Its .promise() is a read-only view that can only be observed. Hand out the promise and keep the Deferred private.
+------------------- $.Deferred() -------------------+
| resolve() reject() notify() <- producer side |
| +------------------ .promise() -----------------+ |
| | done() fail() always() progress() | |
| | then() catch() state() <- consumer side | |
| +-----------------------------------------------+ |
+-----------------------------------------------------+
state: "pending" --resolve--> "resolved"
--reject---> "rejected"
Example: wrapping a callback API
function loadImage(src) {
const deferred = $.Deferred(); // producer side, kept private
const img = new Image();
img.onload = () => deferred.resolve(img); // fulfil with the image
img.onerror = () => deferred.reject(new Error(`Cannot load ${src}`));
img.src = src;
return deferred.promise(); // consumers cannot resolve it themselves
}
loadImage('/photo.jpg')
.done(img => $('#gallery').append(img)) // success callback
.fail(err => console.error(err.message)) // failure callback
.always(() => $('#spinner').hide()); // either way
Progress notifications. Unlike native promises, Deferreds can report progress before they settle:
function countdown(seconds) {
const d = $.Deferred();
let left = seconds;
const id = setInterval(() => {
d.notify(--left); // progress update
if (left === 0) { clearInterval(id); d.resolve(); }
}, 1000);
return d.promise();
}
countdown(3).progress(n => $('#timer').text(n)).done(() => $('#timer').text('Go!'));
done vs then.
.done() / .fail() | .then() (jQuery 3) | |
|---|---|---|
| Returns | The same promise | A new promise (allows chaining transformations) |
| Return value of the callback | Ignored | Becomes the value of the next promise |
| Callback timing | Synchronous when the Deferred settles | Always asynchronous (Promises/A+) |
| Exception thrown in the callback | Propagates as a normal exception | Becomes a rejection of the next promise |
$.getJSON('/api/user/1')
.then(user => $.getJSON(`/api/orders?user=${user.id}`)) // returning a promise chains it
.then(orders => orders.filter(o => o.open))
.then(open => renderOrders(open))
.catch(err => showError(err)); // .catch exists since jQuery 3.0
In jQuery 1.x and 2.x, .then() was not Promises/A+ compliant: callbacks could run synchronously and a thrown exception escaped instead of rejecting the chain. Upgrading to jQuery 3 can therefore change the timing and error handling of existing code.
Combining operations with $.when. $.when is jQuery's counterpart to Promise.all:
$.when($.getJSON('/api/user'), $.getJSON('/api/settings'))
.done(function (userArgs, settingsArgs) {
// With several AJAX promises, each argument is [data, textStatus, jqXHR]
const user = userArgs[0];
const settings = settingsArgs[0];
startApp(user, settings);
})
.fail(function (jqXHR) {
// rejects as soon as ANY of them fails (like Promise.all)
});
The argument shape is a frequent source of bugs: with a single AJAX promise, $.when(p).done((data, status, xhr) => ...) receives the three values separately, with several promises each value is an array.
Interoperating with native promises and async/await. A Deferred's promise is a thenable, so native code accepts it directly:
async function loadDashboard() {
try {
const user = await $.getJSON('/api/user'); // await takes only the FIRST resolved value (data)
const [orders, stats] = await Promise.all([ // mix jQuery and native promises freely
$.getJSON(`/api/orders?user=${user.id}`),
fetch('/api/stats').then(r => r.json())
]);
render(user, orders, stats);
} catch (jqXHRorError) {
// a failed $.ajax rejects with the jqXHR object, not with an Error
console.error(jqXHRorError.status ?? jqXHRorError.message);
}
}
const nativePromise = Promise.resolve($.ajax('/api/ping')); // convert to a real Promise
| Feature | jQuery Deferred | Native Promise |
|---|---|---|
| Resolve with several values | Yes (resolve(a, b, c)) | No, one value |
| Progress notifications | Yes (notify, progress) | No |
| Context for callbacks | Yes (resolveWith(ctx, args)) | No |
| Separate read-only view | .promise() | The promise is already read-only |
| Combinator | $.when | Promise.all, allSettled, race, any |
Works with await | Yes (thenable) | Yes |
| Available in slim build | No (from jQuery 4) | Always |
Key Takeaways
- A Deferred is resolved or rejected by its owner; its
.promise()is the read-only view to hand out. .done(),.fail(),.always()and.progress()register callbacks and return the same promise;.then()returns a new, chainable promise.- Since jQuery 3,
.then()follows Promises/A+: asynchronous callbacks and exceptions converted to rejections. $.when()combines promises, but its callback arguments are arrays per AJAX request when there is more than one.- jQuery promises are thenables, so they work with
awaitandPromise.all; a failed$.ajaxrejects with ajqXHR, not anError.
Practice
- (Beginner) Wrap
setTimeoutin a functionwait(ms)that returns a jQuery promise, and use it to show three messages one second apart with a chain of.then()calls.- (Intermediate) Load three JSON files in parallel with
$.when, display them once all have arrived, and show which file failed if one does. Then rewrite the code withasync/awaitandPromise.allSettled, keeping the same behaviour.- (Interview) "A bug inside a
.then()callback threw anUncaught TypeErrorwith jQuery 1.11. After upgrading to jQuery 3, the page only logs ajQuery.Deferred exceptionwarning and the code continues as if the request had failed. Why, and how do you make such errors visible and handled?" Hint: what does Promises/A+ do with exceptions thrown in callbacks, and what dojQuery.Deferred.exceptionHookand a final.catch()give you?
<a id="83-extending-and-migrating"></a>
8.3 Extending and Migrating
This subchapter covers how jQuery is extended with plugins and jQuery UI, how it coexists with other libraries and frameworks, how it compares with modern standard APIs, and how to upgrade it or remove it from an existing codebase.
Writing jQuery Plugins
jQuery's huge ecosystem grew from one simple fact: every jQuery object inherits from jQuery.fn. Add a function to jQuery.fn and every wrapped set gains a new method, used exactly like a built-in one. This is how thousands of plugins (sliders, validators, date pickers, data tables) were built and distributed.
Analogy: jQuery.fn is the shared toolbox of every jQuery object. Putting a new tool in the box makes it available to every set, past and future, because they all reach into the same box through the prototype chain.
Extension points.
| Extension point | Adds | Example use |
|---|---|---|
$.fn.name = function () {} | A method on wrapped sets | $('.x').highlight() |
$.name = function () {} | A static utility | $.formatCurrency(9.5) |
$.fn.extend({...}) / $.extend({...}) | Several methods or utilities at once | Plugin bundles |
$.easing.name | An easing function for effects | .animate({...}, 400, 'easeOutCubic') |
$.expr.pseudos.name | A custom selector (Sizzle path, slow) | $('a:external') |
$.event.special.name | Custom event behaviour (setup, teardown) | Synthetic gestures |
$.ajaxPrefilter, $.ajaxTransport | Request pre-processing or new transports | Auth headers, mocking |
A minimal plugin. The essential rules: wrap the code in an IIFE that receives jQuery as $, iterate with this.each, and return this so the plugin can be chained.
(function ($) { // local $ is always jQuery (see noConflict)
'use strict';
$.fn.highlight = function (options) {
// Merge user options over the public defaults into a NEW object
const settings = $.extend({}, $.fn.highlight.defaults, options);
return this.each(function () { // `this` inside .each is a DOM element
const $el = $(this);
$el.css({ backgroundColor: settings.color, transition: `background-color ${settings.duration}ms` });
setTimeout(() => $el.css('backgroundColor', ''), settings.duration * 4);
}); // .each returns the set: chaining works
};
// Public defaults: users can change them globally once
$.fn.highlight.defaults = { color: '#fff59d', duration: 300 };
})(jQuery);
// Usage
$('.updated').highlight().addClass('seen'); // chainable
$('#total').highlight({ color: '#c8e6c9' }); // per-call options
$.fn.highlight.defaults.duration = 150; // global change
$.extend for options. $.extend(target, ...sources) copies properties from the sources into target and returns it. Always pass {} as the target, otherwise you modify the defaults object. Pass true as the first argument for a deep merge of nested objects.
$.extend({}, { a: 1, nested: { x: 1, y: 2 } }, { nested: { x: 9 } });
// { a: 1, nested: { x: 9 } } shallow: nested object replaced
$.extend(true, {}, { a: 1, nested: { x: 1, y: 2 } }, { nested: { x: 9 } });
// { a: 1, nested: { x: 9, y: 2 } } deep: nested objects merged
Stateful plugins. Widgets such as a counter or a carousel have state and public methods (reset, destroy). The common pattern stores one instance per element with .data() and dispatches string arguments to methods:
(function ($) {
const NAME = 'counter';
class Counter {
constructor(element, options) {
this.$el = $(element);
this.options = $.extend({}, $.fn[NAME].defaults, options);
this.value = this.options.start;
this.$el.on(`click.${NAME}`, () => this.increment()); // namespaced for clean removal
this.render();
}
increment() { this.value += this.options.step; this.render(); this.$el.trigger(`${NAME}:change`, [this.value]); }
reset() { this.value = this.options.start; this.render(); }
render() { this.$el.text(this.value); }
destroy() {
this.$el.off(`.${NAME}`).removeData(NAME).empty(); // undo everything init did
}
}
$.fn[NAME] = function (optionsOrMethod, ...args) {
return this.each(function () {
let instance = $.data(this, NAME);
if (!instance) {
if (typeof optionsOrMethod === 'string') {
$.error(`${NAME}: cannot call "${optionsOrMethod}" before initialisation`);
}
instance = new Counter(this, optionsOrMethod); // first call: initialise
$.data(this, NAME, instance); // guard against double init
} else if (typeof optionsOrMethod === 'string') {
if (optionsOrMethod.startsWith('_') || typeof instance[optionsOrMethod] !== 'function') {
$.error(`${NAME}: no public method "${optionsOrMethod}"`);
}
instance[optionsOrMethod](...args); // later calls: method dispatch
}
});
};
$.fn[NAME].defaults = { start: 0, step: 1 };
})(jQuery);
$('.counter').counter({ step: 5 });
$('.counter').on('counter:change', (e, value) => console.log(value));
$('.counter').counter('reset');
$('.counter').counter('destroy');
Packaging for module systems. Plugins are often loaded both with a <script> tag and through a bundler. A Universal Module Definition (UMD) wrapper supports both:
(function (factory) {
if (typeof define === 'function' && define.amd) define(['jquery'], factory); // AMD
else if (typeof module === 'object' && module.exports) module.exports = factory(require('jquery')); // CommonJS
else factory(window.jQuery); // global
})(function ($) {
$.fn.highlight = function () { /* ... */ return this; };
return $;
});
Plugin checklist.
- Return
this(orthis.each(...)) unless the method is a getter. - Never assume a single element: work on every element in the set.
- Use one name on
$.fn; expose further actions through method strings, not extra globals. - Namespace events (
.myPlugin) and data keys, and providedestroy. - Do not depend on a global
$; receivejQueryas a parameter. - Escape user-provided content (
.text()), never build HTML from it.
Key Takeaways
- A plugin is a function added to
$.fn, so it becomes available on every jQuery object through the prototype. - Wrap plugins in an IIFE receiving
jQueryas$, iterate withthis.each, and returnthisfor chaining. - Merge options with
$.extend({}, defaults, options)and expose defaults publicly. - Stateful plugins store an instance with
.data(), dispatch method names passed as strings, and must offer adestroythat removes namespaced events and data.
Practice
- (Beginner) Write a
$.fn.maxLength(n)plugin that truncates the text of every matched element toncharacters, adds an ellipsis and stores the full text in atitleattribute. Make sure it is chainable.- (Intermediate) Turn the
counterexample into aratingplugin (five clickable stars) with optionsmaxandvalue, methodsvalue(getter/setter) anddestroy, and arating:changeevent. The getter must return a value instead of the jQuery object.- (Interview) "Your plugin works on its own but breaks when a page initialises it twice on the same element. What safeguards should a jQuery plugin have?" Hint: think about instance storage with
.data(), duplicate event handlers, and whatdestroymust undo.
The jQuery UI Ecosystem
jQuery itself handles selection, events, effects and AJAX, but not ready-made interface components. jQuery UI, an official sister project, filled that gap with themeable widgets and interactions built on a shared architecture. Around it grew a large third-party ecosystem that many existing sites still depend on.
What jQuery UI contains.
| Category | Components |
|---|---|
| Interactions | Draggable, Droppable, Resizable, Selectable, Sortable |
| Widgets | Accordion, Autocomplete, Button, Checkboxradio, Controlgroup, Datepicker, Dialog, Menu, Progressbar, Selectmenu, Slider, Spinner, Tabs, Tooltip |
| Effects | Colour animation, animated class changes (.addClass(cls, duration)), effects such as bounce, explode, shake, extra easings |
| Utilities | Position (place an element relative to another), Widget Factory |
| Theming | CSS framework classes (ui-widget, ui-state-active ...) and the ThemeRoller tool |
A consistent API. Every widget is built with the Widget Factory, so all of them are used the same way:
// 1. Initialise with options
$('#confirm').dialog({
autoOpen: false,
modal: true,
buttons: {
Delete() { deleteItem(); $(this).dialog('close'); },
Cancel() { $(this).dialog('close'); }
},
close() { console.log('closed'); } // callback option
});
// 2. Call methods by name
$('#confirm').dialog('open');
// 3. Read and change options after initialisation
$('#confirm').dialog('option', 'title', 'Delete item?');
const isModal = $('#confirm').dialog('option', 'modal');
// 4. Listen to events: widget name + event name
$('#confirm').on('dialogclose', () => console.log('dialogclose event'));
// 5. Tear down: restore the original markup
$('#confirm').dialog('destroy');
// Interactions follow the same pattern
$('#tasks').sortable({ handle: '.grip', update: (e, ui) => saveOrder($(e.target).sortable('toArray')) });
$('#city').autocomplete({ source: '/api/cities', minLength: 2 });
$('#date').datepicker({ dateFormat: 'yy-mm-dd', minDate: 0 });
Writing a widget with the Widget Factory. $.widget() creates a stateful plugin with option handling, method dispatch, events and destroy already implemented, which is the boilerplate written by hand in the previous topic:
$.widget('demo.stepper', { // namespace.name -> $('#x').stepper()
options: { value: 0, step: 1, change: null }, // 'change' callback also fires 'stepperchange'
_create() { // called once on initialisation
this.element.addClass('stepper');
this._on({ click: 'increment' }); // auto-namespaced, auto-removed on destroy
this._refresh();
},
increment() { // public: $('#x').stepper('increment')
this._setOption('value', this.options.value + this.options.step);
this._trigger('change', null, { value: this.options.value });
},
_setOption(key, value) { // methods starting with _ are private
this._super(key, value); // call the parent implementation
if (key === 'value') this._refresh();
},
_refresh() { this.element.text(this.options.value); },
_destroy() { this.element.removeClass('stepper').empty(); }
});
The wider ecosystem. Many popular libraries were jQuery plugins, and several still are:
| Library | Purpose | Status / modern alternative |
|---|---|---|
| Bootstrap 3 and 4 JavaScript | Modals, dropdowns, tooltips | Bootstrap 5 dropped the jQuery dependency |
| jQuery Validation | Form validation | Constraint Validation API (required, pattern, setCustomValidity) |
| DataTables | Sortable, paginated tables | Still maintained; offers a new DataTable(...) API but still uses jQuery internally |
| Select2, Chosen | Searchable select boxes | Tom Select, framework components |
| Slick, Owl Carousel | Carousels | CSS scroll snap, Swiper |
| jQuery Mobile | Mobile UI framework (Chapter 15.3) | Deprecated |
Status and alternatives. jQuery UI still receives security and compatibility releases, but the project has been in maintenance-only mode since 2021: no new features are planned. The platform now covers many of its widgets natively:
| jQuery UI widget | Native or modern replacement |
|---|---|
| Dialog | <dialog> with showModal() |
| Datepicker | <input type="date"> (or a dedicated library when styling or ranges matter) |
| Accordion | <details> / <summary> (with the name attribute for exclusive groups) |
| Tooltip | title, the Popover API, CSS anchor positioning |
| Progressbar | <progress> |
| Slider, Spinner | <input type="range">, <input type="number"> |
| Position utility | CSS anchor positioning, Floating UI |
| Sortable, Draggable | HTML Drag and Drop API, SortableJS |
Key Takeaways
- jQuery UI provides interactions, widgets, effects and theming, all built on the Widget Factory.
- Every widget follows the same API: initialise with options, call methods by name, get or set options with
'option', listen towidgetnameeventevents, anddestroy. $.widget()gives custom widgets option handling, private methods, events and cleanup for free.- jQuery UI is in maintenance mode, and native elements such as
<dialog>,<details>,<progress>and date inputs now replace many of its widgets.
Practice
- (Beginner) Build a page with jQuery UI Tabs and an Accordion using the CDN version of jQuery UI and a ThemeRoller theme. Switch tabs programmatically with the
optionmethod.- (Intermediate) Implement the same "Delete item?" confirmation once with the jQuery UI Dialog and once with the native
<dialog>element. Compare keyboard handling (Escape, focus trapping, focus return), accessibility and code size.- (Interview) "A legacy page uses jQuery UI Datepicker on 40 forms. Would you replace it with
<input type="date">? What do you need to check first?" Hint: consider date formats sent to the server, locale display, minimum/maximum constraints, browser support and design requirements.
noConflict and Coexistence
jQuery claims two global names: jQuery and its short alias $. Other code may want the same names: older libraries such as Prototype.js and MooTools, other copies of jQuery loaded by plugins or CMS themes, and modern frameworks that manage the same DOM. Coexistence is about letting all of them run on one page without overwriting each other.
How noConflict works. When jQuery loads, it remembers whatever window.$ and window.jQuery held before. jQuery.noConflict() gives those values back.
Before jQuery loads: window.$ = PrototypeJS$ window.jQuery = undefined
After jQuery loads: window.$ = jQuery window.jQuery = jQuery
jQuery.noConflict(): window.$ = PrototypeJS$ window.jQuery = jQuery
jQuery.noConflict(true): window.$ = PrototypeJS$ window.jQuery = undefined (previous value)
<script src="prototype.js"></script> <!-- defines its own $ -->
<script src="jquery.min.js"></script> <!-- overwrites $ -->
<script>
const jq = jQuery.noConflict(); // $ is Prototype's again; jq is jQuery
$('header'); // Prototype: element with id "header"
jq('header'); // jQuery: all <header> elements
</script>
Writing code that never depends on the global $. Two idioms keep $ local, so your code works whether or not noConflict was called:
// 1. IIFE receiving jQuery as a parameter (used by plugins)
(function ($) {
$('.x').hide(); // $ is jQuery inside, whatever it is outside
})(jQuery);
// 2. Ready handler: jQuery passes itself as the first argument
jQuery(function ($) {
$('.x').show();
});
WordPress, for example, loads its bundled jQuery in no-conflict mode, so theme and plugin code that uses a bare $ fails with "$ is not a function". The two idioms above are the standard fix.
Multiple jQuery versions on one page. A legacy plugin may need jQuery 1.x while new code wants 3.x. Plugins attach themselves to whichever jQuery is global at the moment they load, so load order is everything:
<script src="jquery-1.12.4.min.js"></script>
<script src="legacy-slider.js"></script> <!-- registers $.fn.slider on 1.12 -->
<script src="jquery-3.7.1.min.js"></script>
<script>
const jq3 = jQuery.noConflict(true); // restore globals to 1.12, keep 3.7 in jq3
console.log(jQuery.fn.jquery, jq3.fn.jquery); // "1.12.4" "3.7.1"
jQuery('#hero').slider(); // legacy plugin on 1.12
jq3('#app').addClass('ready'); // new code on 3.7
</script>
This works, but doubles the download and keeps a vulnerable version alive. Treat it as a temporary bridge.
A classic symptom: "$(...).modal is not a function". It almost always means jQuery was loaded twice: the plugin registered on the first copy, then a second <script> (often from a theme or plugin) replaced window.jQuery with a fresh copy that has no plugins.
jquery.js (copy A) -> bootstrap.js adds A.fn.modal -> jquery.js (copy B) replaces window.jQuery
-> $('#m').modal() looks in B.fn: undefined
Check with jQuery.fn.jquery in the console and search the page source for every jQuery <script>.
jQuery with bundlers and ES modules. With npm and a bundler, jQuery is imported instead of global. Old plugins that expect window.jQuery must see it before they run:
// app.js
import $ from 'jquery';
window.$ = window.jQuery = $; // make it global for plugins that expect it
await import('legacy-plugin'); // dynamic import: runs AFTER the assignment above
// (static imports are hoisted and would run before the assignment)
$('#el').legacyPlugin();
Bundlers can also inject the variable automatically (for example webpack's ProvidePlugin mapping $ and jQuery to the jquery module).
Coexisting with React, Vue and similar frameworks. A framework assumes it alone controls the DOM it renders (Chapter 14). If jQuery changes those nodes, the next render may overwrite the changes or fail. The safe pattern is to give jQuery a node the framework will not touch, initialise in a mount effect and clean up on unmount:
import { useEffect, useRef } from 'react';
import $ from 'jquery';
import 'jquery-ui/ui/widgets/datepicker';
function DateField({ onChange }) {
const inputRef = useRef(null);
useEffect(() => {
const $input = $(inputRef.current);
$input.datepicker({ onSelect: (text) => onChange(text) }); // jQuery owns this element's behaviour
return () => $input.datepicker('destroy'); // clean up on unmount
}, [onChange]);
return <input ref={inputRef} />; // React renders it once and does not manage its contents
}
Key Takeaways
jQuery.noConflict()restores the previous$;noConflict(true)also restores the previousjQuery, which allows several versions on one page.- Write code that receives jQuery as a parameter (IIFE or
jQuery(function ($) { ... })) so it never depends on the global$. - Plugins register on the jQuery that is global when they load; loading jQuery twice discards previously registered plugins.
- With bundlers, expose
window.jQuerybefore loading plugins that expect a global. - Inside frameworks, let jQuery manage only nodes the framework does not re-render, and destroy its widgets on unmount.
Practice
- (Beginner) Create a page that defines
window.$ = (id) => document.getElementById(id)before loading jQuery. CalljQuery.noConflict()and show that both the custom$and jQuery work, using the IIFE idiom for the jQuery code.- (Intermediate) Reproduce the "
$(...).modal is not a function" error by loading jQuery, Bootstrap 4 and then jQuery again. Fix it in two ways: by removing the duplicate, and by usingnoConflict(true)to keep both.- (Interview) "How would you integrate a jQuery plugin such as Select2 into a React component without React and jQuery fighting over the DOM?" Hint: think about refs, mount and unmount effects, controlled vs uncontrolled values, and which events need bridging back to React state.
jQuery vs Modern Vanilla JavaScript
The browser features that jQuery papered over have since been standardised, and most of its API now has a direct native equivalent. Knowing the mapping lets you read jQuery code quickly, write new code without the dependency, and judge when keeping jQuery is still the sensible choice.
Side-by-side reference.
| Task | jQuery | Vanilla JavaScript |
|---|---|---|
| Select many | $('.item') | document.querySelectorAll('.item') |
| Select one | $('#main') | document.querySelector('#main') (or getElementById) |
| Search inside | $el.find('a') | el.querySelectorAll('a') |
| Closest ancestor | $el.closest('tr') | el.closest('tr') |
| Parent / siblings | .parent(), .next(), .prev() | parentElement, nextElementSibling, previousElementSibling |
| Classes | .addClass, .removeClass, .toggleClass, .hasClass | classList.add, remove, toggle, contains |
| Attributes | .attr('href'), .attr('href', v) | getAttribute('href'), setAttribute('href', v) |
| Properties | .prop('checked') | el.checked |
| Data | .data('userId') | el.dataset.userId (always a string) |
| Inline style | .css('color', 'red') | el.style.color = 'red' |
| Computed style | .css('color') | getComputedStyle(el).color |
| Text / HTML | .text(), .html() | textContent, innerHTML |
| Insert | .append, .prepend, .before, .after | append, prepend, before, after (same names) |
| Replace / remove | .replaceWith(), .remove() | replaceWith(), remove() |
| Create | $('<li>', { text: 'x' }) | Object.assign(document.createElement('li'), { textContent: 'x' }) |
| Bind / unbind | .on('click', fn), .off('click', fn) | addEventListener, removeEventListener (or an AbortSignal) |
| Delegate | .on('click', '.btn', fn) | addEventListener + event.target.closest('.btn') |
| Trigger | .trigger('cart:updated', [3]) | dispatchEvent(new CustomEvent('cart:updated', { bubbles: true, detail: 3 })) |
| DOM ready | $(fn) | defer / type="module" scripts, or DOMContentLoaded |
| Fade / slide | .fadeIn(), .slideUp() | CSS transitions, el.animate() |
| AJAX | $.getJSON(url) | fetch(url).then(r => r.json()) |
| Deferred | $.Deferred(), $.when() | Promise.withResolvers(), Promise.all() |
| Utilities | $.each, $.map, $.grep, $.inArray, $.trim, $.isArray, $.extend | forEach, map, filter, includes, trim, Array.isArray, spread / Object.assign / structuredClone |
| Serialise form | $(form).serialize() | new URLSearchParams(new FormData(form)).toString() |
The behavioural differences that bite. The names map one to one; the semantics do not always.
| jQuery behaviour | Native behaviour |
|---|---|
| Methods act on every matched element | You loop: els.forEach(el => ...) |
| An empty set silently does nothing | querySelector returns null, and null.classList throws |
| Getters read the first element | You choose the element explicitly |
.data() converts types and caches | dataset always returns strings and writes to the attribute |
.html() executes inline <script> elements | innerHTML never executes scripts |
$.ajax fails on HTTP 4xx/5xx | fetch resolves; check response.ok |
return false stops default and propagation | Return value ignored |
.trigger('custom') runs jQuery handlers only (for non-native events) | dispatchEvent reaches every listener |
Example: a tabs component, both ways
<div class="tabs">
<button class="tab active" data-panel="p1">One</button>
<button class="tab" data-panel="p2">Two</button>
<section id="p1" class="panel">First panel</section>
<section id="p2" class="panel" hidden>Second panel</section>
</div>
// jQuery
$('.tabs').on('click', '.tab', function () {
const $tabs = $(this).closest('.tabs');
$tabs.find('.tab').removeClass('active');
$(this).addClass('active');
$tabs.find('.panel').attr('hidden', true);
$('#' + $(this).data('panel')).removeAttr('hidden');
});
// Vanilla JavaScript
document.querySelectorAll('.tabs').forEach(tabs => {
tabs.addEventListener('click', event => {
const tab = event.target.closest('.tab');
if (!tab || !tabs.contains(tab)) return; // click outside a tab: ignore
tabs.querySelectorAll('.tab').forEach(t => t.classList.toggle('active', t === tab));
tabs.querySelectorAll('.panel').forEach(p => { p.hidden = p.id !== tab.dataset.panel; });
});
});
A tiny helper layer. Most of jQuery's convenience comes from a few patterns. A small helper module recovers them without a dependency:
// dom.js
export const $ = (sel, root = document) => root.querySelector(sel);
export const $$ = (sel, root = document) => [...root.querySelectorAll(sel)]; // real array
export function on(root, type, selector, handler, options) {
root.addEventListener(type, event => {
const match = event.target.closest(selector);
if (match && root.contains(match)) handler.call(match, event, match); // `this` like jQuery
}, options);
}
// usage
on(document.body, 'click', '.delete', function () { this.closest('li').remove(); });
When does jQuery still make sense?
| Keep or use jQuery | Prefer vanilla or a framework |
|---|---|
| The codebase already uses it heavily and works | New projects or new, isolated modules |
| You depend on jQuery plugins with no good alternative | Bundle size and performance budgets matter (jQuery is roughly 30 KB minified and gzipped) |
| WordPress or a CMS theme already ships it | You use React, Vue, Svelte or Angular, which manage the DOM themselves |
| Quick scripts for internal tools by a jQuery-fluent team | Code where TypeScript and tree-shaking are important |
Key Takeaways
- Almost every jQuery method has a direct standard counterpart:
querySelectorAll,closest,classList,append,addEventListener,fetch, promises. - The differences lie in semantics: native APIs do not iterate implicitly,
nullresults throw,datasethas no type conversion,fetchdoes not fail on HTTP errors. - A few small helpers (
$,$$, delegatedon) cover most of what people miss when removing jQuery. - jQuery remains reasonable in existing code, plugin-dependent pages and CMS environments; new code rarely needs it.
Practice
- (Beginner) Translate ten lines of jQuery from any earlier example in this chapter into vanilla JavaScript using the reference table. Note every place where you had to add a loop or a
nullcheck.- (Intermediate) Extend
dom.jswithtoggle(el, show),getJSON(url)(which rejects on HTTP errors like$.getJSON) and aserialize(form)function, then rewrite the AJAX form example of 8.2 using only these helpers.- (Interview) "If browsers now implement everything jQuery offered, why did it remain on a majority of websites for so long?" Hint: consider CMS defaults, plugin ecosystems, the cost and risk of rewriting working code, and who maintains most websites.
Migration Strategies
"Migrating" a jQuery codebase means one of two projects: upgrading to a newer jQuery version, usually for security fixes and browser support, or removing jQuery in favour of standard APIs or a framework. Both are refactors of code that already works, so the goal is to change it step by step without breaking behaviour.
Why upgrade at all. Old jQuery versions have published vulnerabilities, and security scanners flag them:
| Fixed in | Issue |
|---|---|
| 3.0 | Cross-domain AJAX responses could be executed as scripts when dataType was not set (CVE-2015-9251) |
| 3.4 | Prototype pollution through $.extend(true, {}, untrustedObject) (CVE-2019-11358) |
| 3.5 | XSS when passing sanitised but untrusted HTML to .html(), .append() and others, caused by the old htmlPrefilter (CVE-2020-11022, CVE-2020-11023) |
Upgrading with jQuery Migrate. The jQuery team publishes the jQuery Migrate plugin, which restores removed APIs and logs a warning every time deprecated behaviour is used. It turns an upgrade into a checklist.
<!-- Step 1: new jQuery + development build of Migrate (logs warnings) -->
<script src="jquery-3.7.1.js"></script>
<script src="jquery-migrate-3.4.1.js"></script>
Console while exercising the application:
JQMIGRATE: Migrate is installed with logging active, version 3.4.1
JQMIGRATE: jQuery.fn.bind() is deprecated
JQMIGRATE: jQuery.fn.click() event shorthand is deprecated
JQMIGRATE: jQuery.isArray is deprecated; use Array.isArray
// Useful switches (set after loading Migrate)
jQuery.migrateTrace = true; // include a stack trace for each warning (default)
console.table(jQuery.migrateWarnings); // every distinct warning recorded so far
Upgrade loop
[ inventory: jQuery version, plugins and their versions ]
|
v
[ load new jQuery + Migrate (dev build) ] <---------------+
| |
v |
[ run tests and click through every feature ] |
| |
v |
[ fix each JQMIGRATE warning; upgrade or replace plugins ]+
| (no warnings left)
v
[ remove Migrate, run tests again, ship ]
From very old versions (before 1.9), upgrade in stages: first to 1.12 or 2.2 with Migrate 1.x, then to 3.x with Migrate 3.x, then to 4.x with its matching Migrate. Plugins are usually the hardest part: an abandoned plugin that uses removed APIs must be patched, replaced or kept alive by Migrate.
Removing jQuery: the strangler approach. Rewriting everything in one go is risky. Instead, replace jQuery gradually, the way a strangler fig slowly grows around a tree until the old tree is gone:
Phase 1: stop the growth Phase 2: replace by module Phase 3: remove
+---------------------+ +---------------------+ +---------------------+
| lint rule forbids | | module A: vanilla | | no jQuery usage |
| NEW jQuery usage | ---> | module B: vanilla | ---> | left: delete the |
| new code: vanilla | | module C: jQuery | | <script>, measure |
| or framework | | plugins replaced | | size and speed |
+---------------------+ +---------------------+ +---------------------+
- Inventory. Search for
$(,jQuery,$.and plugin method calls. The ESLint plugineslint-plugin-no-jqueryreports every jQuery method used, which gives you a measurable backlog. - Stop the growth. Enable a lint rule that forbids new jQuery usage, so the backlog only shrinks.
- Write tests first. End-to-end tests (Chapter 17) that click through features catch the subtle behavioural differences listed in the previous topic.
- Replace plugins. Plugins are hard dependencies; find native or framework replacements (see The jQuery UI Ecosystem) before removing jQuery itself.
- Convert module by module, using a small helper layer rather than re-implementing jQuery.
- Remove the
<script>tag (or the npm dependency) once usage reaches zero, then measure bundle size and load time.
Example: converting one module step by step
// Before
$(function () {
$('#newsletter').on('submit', function (e) {
e.preventDefault();
const $form = $(this);
$.post('/subscribe', $form.serialize())
.done(() => $form.replaceWith('<p class="ok">Subscribed</p>'))
.fail(xhr => $form.find('.error').text(xhr.responseJSON?.message || 'Error').show());
});
});
// After: same behaviour, including failure on HTTP errors
const form = document.querySelector('#newsletter');
form?.addEventListener('submit', async (e) => { // ?. keeps jQuery's "no element, no error"
e.preventDefault();
const error = form.querySelector('.error');
try {
const response = await fetch('/subscribe', {
method: 'POST',
body: new URLSearchParams(new FormData(form)) // same form-encoded body as .serialize()
});
if (!response.ok) { // $.post would have failed here
const body = await response.json().catch(() => ({}));
throw new Error(body.message || 'Error');
}
form.replaceWith(Object.assign(document.createElement('p'), { className: 'ok', textContent: 'Subscribed' }));
} catch (err) {
error.textContent = err.message;
error.hidden = false;
}
});
Migrating to a framework. When the target is React, Vue or similar (Chapter 14), the strangler approach still applies: mount framework components into specific containers of the existing page ("islands"), let them own those containers completely, and communicate with remaining jQuery code through DOM custom events or a small shared store until it is gone.
Pitfalls during a mixed period.
| Pitfall | Why it happens | Mitigation |
|---|---|---|
| Native listeners miss custom events | .trigger('custom') does not dispatch a real DOM event for non-native types | Dispatch CustomEvent from both sides, or bridge with a jQuery handler that re-dispatches |
Data set with .data() invisible to dataset | Separate jQuery cache | Use .attr('data-x') or dataset in shared code |
| Scripts in loaded HTML stop running | .html() executed them, innerHTML does not | Move behaviour out of injected HTML |
| Error handling silently changes | fetch resolves on 404 or 500 | Check response.ok in a shared helper |
| Elements not found now throw | null instead of an empty set | Optional chaining or explicit guards |
Key Takeaways
- Upgrades fix known vulnerabilities; use jQuery Migrate's development build to find and fix every deprecated usage, then remove it.
- Upgrade very old versions in stages and treat plugins as the main risk.
- Remove jQuery gradually: inventory, block new usage with a linter, add tests, replace plugins, convert module by module, then delete the dependency.
- During the mixed period, watch for semantic differences: custom events,
.data()vsdataset, script execution in.html()andfetcherror handling.
Practice
- (Beginner) Load an old jQuery 1.x example page with jQuery 3.7 and the development build of jQuery Migrate. List every
JQMIGRATEwarning, fix each one, then remove Migrate and confirm the page still works.- (Intermediate) Take a small jQuery application (for example the to-do list from 8.2) and remove jQuery using the strangler steps: add
eslint-plugin-no-jquery, write two Playwright tests, then convert it module by module until the jQuery<script>can be deleted. Record the bundle size before and after.- (Interview) "Your security scanner flags jQuery 1.11 on a large legacy site with 30 plugins. Management asks whether to upgrade jQuery or remove it entirely. How do you decide and plan the work?" Hint: weigh the size of the jQuery and plugin footprint, the available test coverage, the plans for the site's future, and whether an upgrade with Migrate can remove the immediate risk first.
<a id="9-typescript"></a>