Articles
jQuery Interview Questions: Event Delegation, Data Caching, and Ajax Races
Practice jQuery interview questions with delegated events, repeated mounts, data-cache surprises, Ajax races, loading ownership, and cleanup tests.
A delete button works for existing rows but fails for newly inserted rows. Remounting the widget makes one click delete twice. Meanwhile, an older search result overwrites the latest one. These jQuery interview questions ask you to establish three owners: where the handler lives, where element data lives, and which request may update the interface.
This guide focuses on maintaining an existing jQuery application. Official API facts are linked at first use. Our browser observations use jQuery 4.0.0 from the official distribution, with original DOM fixtures and injected Deferred-based transport fakes. The Ajax cases do not exercise a real network or server. Start with Implement debounced async autocomplete input, then explain its subscription and request lifetimes.
Why does event delegation handle newly inserted rows?
Official fact: .on() can attach a direct handler or a delegated handler using a selector. Delegation attaches the subscription to an existing ancestor and evaluates matching descendants as an event bubbles through it. jQuery on documentation
For our original list, this direct subscription binds only the buttons selected at that moment:
$('#list button.remove').on('click', removeNote);
A button appended afterward did not exist in that selection. The delegated version puts the handler on the persistent list:
$('#list').on('click.notes', 'button.remove', function (event) {
event.preventDefault();
const noteId = $(this).attr('data-note-id');
removeNote(noteId);
});
removeNote is an application function, not a jQuery API. For this contract, it receives a validated note identifier and manages the business action separately. The example uses an attribute because the identifier's declared source of truth is the DOM attribute; the cache discussion below explains why that choice matters.
The #list element must survive replacement of its child rows. Replacing #list itself destroys that particular attachment; new markup with the same ID does not inherit the old element's handlers. Attach to a stable component root and remove the subscription when its owner is disposed.
Delegation also depends on event propagation. A child handler that stops propagation can prevent the event from reaching the ancestor. Do not say delegation handles every event regardless of its bubbling behavior. Explain the event type and route involved in the actual exercise.
What are this, target, and delegateTarget?
Official fact: in a delegated handler, the normal function's this identifies the matching element. event.target is the event's originating element. event.delegateTarget identifies the element where the delegated handler was attached. Delegated handlers, delegateTarget reference
Our browser fixture placed a span inside a remove button under #list. Triggering the span's click produced this observed trace:
| Expression | Observed value | Meaning in this fixture |
|---|---|---|
this.tagName | BUTTON | The selector matched the remove control |
event.target.tagName | SPAN | The click originated inside the button |
event.delegateTarget.id | list | The list owns the subscription |
Reading an ID from event.target would fail when the span has no such attribute. Using an arrow function and expecting jQuery to supply the same this would change the meaning of the code. An arrow captures lexical this; use the event object deliberately or keep the normal function when relying on the documented binding.
Keep the selector narrow. Matching both a child and an ancestor can produce more than one matching invocation along the event route. For our list, the remove control is the single intended match. A broad selector such as every element with an action attribute requires a more careful contract for nested actions.
How do repeated mounts create duplicate handlers?
Official fact: .off() can remove handlers by event, selector, handler identity, or namespace. The off documentation recommends namespaces to avoid removing unrelated code's handlers.
In our observed baseline, mounting twice attached two delegated subscriptions. One synthetic click incremented the counter twice. The repaired fixture removed its namespace before attaching its handler:
function mountNotes() {
$('#list').off('.notes').on('click.notes', 'button.remove', onRemove);
}
function disposeNotes() {
$('#list').off('.notes');
}
After two mounts, the same click incremented the counter once. This result tests subscription duplication; it does not prove that a server mutation is idempotent or that every component lifecycle is correct.
The namespace must belong to that widget. If several widgets intentionally share one root, assign distinct namespaces or use explicit handler identities so one widget's cleanup does not remove another's behavior. Calling .off() without filters is a much broader action than removing the widget's own subscription.
Also separate subscription cleanup from request cleanup. Removing the input handler prevents new searches, but an already started request can still finish. Disposing the component must invalidate that request's right to render, as the controller later in this guide demonstrates.
Why can data() disagree with a data attribute?
Official fact: .data() reads data-* attributes into jQuery's internal data storage and performs supported value conversion. Subsequent reads can use the cached value. Writing with .data() does not update the DOM attribute. Data documentation
Our fixture created data-order-id="42", read it with .data('orderId'), changed the attribute to "43", and read it again. The jQuery4.0.0 browser fixture produced these values:
first .data('orderId') 42 number
after .attr('data-order-id', '43')
.data('orderId') 42 cached number
.attr('data-order-id') "43" string
after .removeData('orderId')
.data('orderId') 43 reread number
Official fact: .removeData() removes jQuery's stored data, not the corresponding HTML attribute. A later data read can recover the attribute value. RemoveData documentation
The repair is an ownership decision. If the attribute is the source of truth, read and write that attribute consistently and normalize its value according to the API contract. If jQuery data owns the state, use .data() consistently and do not assume that inspecting markup shows its current value.
Before clearing a cache, identify which storage location owns the value. State which writer changes the value, which reader consumes it, and whether a second storage location is necessary. This makes numeric conversion easier to explain and test: an identifier and a quantity may require different types even when both look like digits.
Our observed 42 and 43 conversions are specific test inputs. The documentation describes conversion rules that preserve some strings rather than converting every numeric-looking value. Verify the actual value and version when interpreting a legacy application's state.
Why is abort insufficient as a result-ownership policy?
Official fact: $.ajax() returns a jqXHR with abort() and Deferred-style .done(), .fail(), and .always() callbacks. Failures can include statuses such as abort, timeout, or parsererror. Ajax documentation
Calling abort requests cancellation of the previous transport; the fixture separately checks who may update the UI. The application still needs to define which started operation owns visible state. That policy also covers injected transports, subsequent asynchronous work, disposal, and the loading indicator. The original controller below grants ownership to the newest started request:
function makeSearchController(ajax, render, loading, error) {
let generation = 0, pending = null, disposed = false;
function search(query) {
if (disposed) return;
const mine = ++generation;
pending?.abort();
loading(true);
pending = ajax({url: '/search', data: {q: query}, dataType: 'json'});
pending.done(data => {
if (!disposed && mine === generation) render(data);
});
pending.fail((xhr, status) => {
if (!disposed && mine === generation && status !== 'abort') error(status);
});
pending.always(() => {
if (!disposed && mine === generation) {
pending = null;
loading(false);
}
});
}
function dispose() {
disposed = true;
++generation;
pending?.abort();
pending = null;
}
return {search, dispose};
}
Notice the order: advance generation before aborting the old request. In our Deferred fake, abort can reject synchronously and immediately run failure and completion callbacks. The older generation must already be obsolete when those callbacks execute. Otherwise, cleanup from A can interfere with B's ownership.
Every visible update is guarded. Protecting results while allowing A to display an error or clear B's spinner would leave a race in a different part of the interface. Disposal similarly invalidates completion, even if the fake transport ignores cancellation.
The code intentionally handles newest started request, not every possible search-product requirement. For a debounced search, decide whether a newly typed query must immediately invalidate the old request while the debounce timer waits. Empty-query behavior, the timer, and queued transformations need their own owner. Explain that boundary instead of calling debounce a complete race solution.
What did the deterministic Ajax tests establish?
We used real jQuery Deferred objects with two injected transport behaviors: one rejects immediately on abort, and one deliberately ignores abort so the fixture can deliver obsolete results. These are controlled fakes. The second behavior is not a claim that normal jqXHR cancellation allows the same request to succeed afterward.
| Original fixture | Observed result | What it checks |
|---|---|---|
| B resolves, then obsolete A resolves | Visible value remains B | Newest started request owns rendering |
| Starting B aborts A synchronously | B loading stays true; no abort error | Old callbacks cannot own B's state |
| Current B fails with timeout | Timeout reported; loading becomes false | Current failure and cleanup remain visible |
| Dispose, then A resolves | No late rendering | Owner lifetime ends independently of transport |
Together with the event and data tests, eight fixture checks passed in the local browser. We captured values at the time of each check so a later mutation could not rewrite the evidence. The results establish the illustrated ownership behavior, not server-side cancellation or performance under production load.
For network testing, separately investigate response parsing, HTTP errors, credentials, and the endpoint's mutation semantics. Aborting a client request does not provide an undo transaction for a business action that the server already accepted. Keep that question distinct from whether an obsolete response can change the screen.
How should results and failures enter the DOM?
Official fact: .text() sets textual content rather than interpreting a value as HTML. Text documentation For our search results, render a returned label as text unless the application has an explicit, reviewed HTML contract.
A useful original review case returns a label containing angle brackets. Check that it appears as literal text, not new markup. Validate the response's data shape before reading fields, and decide how an empty result differs from a failed request. The controller's render, loading, and error callbacks are interfaces; their implementations still need these decisions.
Finally, test lifecycle cleanup from the user's sequence: mount, type, start a request, navigate away, and resolve the old request. Inspect subscriptions and visible state, not just whether a console warning appeared. This sequence combines the article's three ownership lifetimes without making them interchangeable.