jQuery delegated event handling: how tightly should the container be scoped for a dynamic list
0 reputation · 05 Aug 2023, 02:49 UTC
0 reputation · 05 Aug 2023, 02:49 UTC
I am wiring click handling for a table whose rows are added and removed at runtime via AJAX, using jQuery 3.7. The documented approach is a single delegated listener, $(container).on('click', 'tr', handler), so new rows work without re-binding. What I cannot settle is where container should live.
Attaching to document would survive any re-render of the table's parent markup, but every click anywhere on the page would then be tested against the tr selector along the full ancestor chain. Attaching to the nearest stable ancestor (the table's wrapper) limits that traversal, but if that wrapper is ever replaced by a partial refresh, the handler silently dies.
The page also has other components that call stopPropagation() in some paths, and I am unsure how much that constrains container choice.
Is there a documented or conventionally accepted rule for choosing the delegation root — closest stable ancestor versus document? Does the per-event selector-matching cost on a broad container matter in practice for low-frequency events like clicks? And should components that call stopPropagation() rule out delegating above them entirely?
32275 reputation · 05 Aug 2023, 13:30 UTC
Delegate on the closest ancestor that is guaranteed to exist for the page's lifetime and is never itself replaced by re-renders. That is the conventionally accepted rule, and it resolves your tension directly: the table's wrapper is the right root only if your partial-refresh code never swaps that wrapper out. If it does get swapped, move up to the next fixed layout element — not necessarily document, but whatever ancestor survives every refresh path.
Delegation works because the handler lives on an element that stays in the DOM while its descendants change. Two failure modes sit at opposite ends of your choice:
event.target up to document, testing your selector at each ancestor. Correct, just broader than necessary.So the practical procedure is: inspect your markup and your refresh code, find the nearest ancestor that no code path ever replaces, and bind there. If your table's wrapper is re-rendered, bind to the persistent page-level container above it instead.
// Bind to an ancestor that partial refreshes never replace
$('#page-content').on('click', 'table.results tr', function (e) {
// this === the clicked tr
});For clicks, no — not measurably. Per event, jQuery walks from the target up to the container and runs a selector test at each step. That is a handful of matches() calls on a user-driven event that fires at most a few times per second. This cost only becomes a real concern for high-frequency events like mousemove or scroll, which you should rarely delegate broadly anyway. Choosing document over a wrapper for click delegation is a robustness/scoping trade-off, not a performance one. If you doubt it in your specific page, profile one click in devtools; the delegation overhead will be lost in the noise.
Yes — components that call stopPropagation() (or stopImmediatePropagation()) on the bubble path between your rows and the delegation root will prevent the event from ever reaching the container. The delegated handler simply never fires, again silently. Two consequences:
document: the higher you bind, the more third-party code sits on the bubble path with a chance to intercept. A closer stable root reduces exposure to other components' propagation handling.Verify by clicking rows while the suspect components are active, or temporarily add a capturing-phase listener that logs whether the event reaches your intended root.
'tr' matches rows in any descendant table, including a nested table inside a cell. If nested tables are possible, qualify the selector (e.g., 'table.results > tbody > tr' or a row class)..on(events, selector, handler) is long-stable across jQuery 3.x, including 3.7 — nothing here is version-sensitive.Bind to the nearest ancestor your refresh logic provably never replaces — a fixed layout container above the swappable wrapper if needed. That gives you bounded selector matching, survival across row-level AJAX updates, and the smallest possible exposure to other components' stopPropagation(). Reserve document for the case where genuinely no stable ancestor exists.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.