Skip to content
Carl Victor Fontanos
Carl Victor Fontanos

Carl Victor Fontanos

Software Engineer

I build web applications and share what I learn along the way.

© 2026

Why Your Click Handlers Die After AJAX (and the One-Line Fix)

C
Carlo Fontanos
· 2 min read

If I had to name the single most-asked jQuery question of all time, it's this bug: delete buttons work fine, then the table refreshes via AJAX, and every button plays dead. No errors, no warnings. Clicks vanish into nothing.

Why it happens

$('.delete-btn').on('click', deleteRow);   // binds to elements that exist NOW

Handlers attach to specific DOM nodes, not to selectors. When AJAX replaces the table, the old nodes (with their handlers) are destroyed; the new buttons are strangers that were never bound. The selector string in your code is a historical record, not a living subscription.

The fix: delegate to something permanent

$('#orders-table').on('click', '.delete-btn', deleteRow);

Same .on(), one extra argument. Now the handler lives on the table (which survives refreshes), and jQuery checks each bubbling click against '.delete-btn' at click time. Buttons created five minutes from now work automatically, because nothing was ever bound to them - the table is doing the listening.

This piggybacks on event bubbling: clicks travel up the tree, so one ancestor can observe descendants that don't exist yet. Bonus: one handler on the table beats 500 handlers on 500 rows for memory, and there's nothing to rebind after every render.

Delegate to the nearest stable ancestor - not document

$(document).on('click', '.delete-btn', deleteRow);   // works, but lazy

Binding everything to document works and is why old Stack Overflow answers recommend it, but every click on the page now runs your selector checks, and debugging "what's bound where" becomes archaeology. The container that gets its contents replaced is the right home.

The vanilla JS version (same idea, two lines)

document.getElementById('orders-table').addEventListener('click', (e) => {
    const btn = e.target.closest('.delete-btn');
    if (btn && e.currentTarget.contains(btn)) deleteRow(btn.dataset.id);
});

closest() walks up from whatever was clicked (an icon inside the button, say) to the matching element - which handles the "clicked the SVG inside the button" case that trips up naive e.target checks. jQuery's delegation does exactly this internally.

Related gotchas from the same family

  • Delegated handlers can't stopPropagation() a direct handler on the target - the event already bubbled to reach you.
  • focus/blur don't bubble: delegate their bubbling cousins focusin/focusout instead.
  • Need a handler to run once ever? once: true in vanilla, .one() in jQuery.

Delegation is twenty-year-old technique that never stopped being the answer. AJAX-refreshed markup, infinite scroll, modals injected on demand - bind to the container, describe the target, done forever.

C
Written by Carlo Fontanos

Full-stack web developer sharing practical tutorials and building tools that ship.

Got something on your mind?

My inbox is open - no forms disappearing into the void here.

  • Just say hello Found a tutorial useful? Spotted a mistake? Tell me.
  • Hire me for a project Have something custom in mind? Let's talk scope and timelines.
  • Product support Bought something here? I'll help you get it running.

I usually reply within 1-2 business days.

Message sent!

Your details are only used to reply to you.

Keep reading