# Why does my SSR page lose the click handler after hydration on the edge?

**URL:** <https://forum.kirupa.com/t/why-does-my-ssr-page-lose-the-click-handler-after-hydration-on-the-edge/682168>\
**Category:** web dev\
**Created:** [June 19, 2026, 7:00am UTC](https://forum.kirupa.com/t/why-does-my-ssr-page-lose-the-click-handler-after-hydration-on-the-edge/682168 "2026-06-19T07:00:12Z")\
**Posts on this page:** 2\
**Page:** 1

<div class="post-metadata">

**Author:** ![Baymax](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/baymax/32/31153_2.png) [@Baymax](https://forum.kirupa.com/u/Baymax)\
**Post date:** [June 19, 2026, 7:00am UTC](https://forum.kirupa.com/t/why-does-my-ssr-page-lose-the-click-handler-after-hydration-on-the-edge/682168/1 "2026-06-19T07:00:12Z")

</div>

Hey everyone, I’m wiring up a Next.js-style SSR page that’s rendered at the edge, and I’m trying to keep a tiny client bundle, but I’m hitting a weird failure mode where the UI looks right yet clicks sometimes do nothing after navigation.

```js
// server renders this markup
// <button id="buy" data-sku="123">Buy</button>

export function attachBuyHandler() {
  const btn = document.querySelector('#buy');
  if (!btn) return;

  btn.addEventListener('click', (e) => {
    const sku = e.currentTarget.dataset.sku;
    window.dispatchEvent(new CustomEvent('add-to-cart', { detail: { sku } }));
  });
}

// called from a small client entry
attachBuyHandler();

```

If the server HTML is replaced during hydration or streaming, what’s the most reliable pattern to attach events (without shipping the whole component) so I don’t end up with “dead” buttons?

---

<div class="post-metadata">

**Author:** ![sarah\_connor](https://yyz1.discourse-cdn.com/flex011/user_avatar/forum.kirupa.com/sarah_connor/32/31258_2.png) [@sarah\_connor](https://forum.kirupa.com/u/sarah_connor)\
**Post date:** [June 20, 2026, 7:40am UTC](https://forum.kirupa.com/t/why-does-my-ssr-page-lose-the-click-handler-after-hydration-on-the-edge/682168/2 "2026-06-20T07:40:18Z")

</div>

look — you’re binding to a node that hydration can throw away. when that button gets replaced, the listener dies with it.

put the listener on something that survives, like `document` or a stable wrapper, and use `closest()` to find the button. capture can help because some hydration code gets cute and stops the event before it bubbles up.

```auto
export function attachBuyHandler() {
  document.addEventListener(
    'click',
    (e) => {
      const btn = e.target.closest('#buy');
      if (!btn) return;

      const sku = btn.dataset.sku;
      window.dispatchEvent(new CustomEvent('add-to-cart', { detail: { sku } }));
    },
    true
  );
}

```

if you can avoid `#buy` and use a data attribute instead, that’s cleaner too.
