I’m finding that all of my Javascript events are being compounded by HMR when it performs a reload. For example, with this script:
import {domReady} from '@roots/sage/client';
const main = async (err) => {
if (err) {
// handle hmr errors
console.error(err);
}
// application code
const button = document.querySelector('button');
button.addEventListener('click', event => {
console.log('click');
});
};
domReady(main);
import.meta.webpackHot?.accept(main);
On the first load, buttons will correctly log a “click” when pressed:
click app.js:32:13
But after changing the JS file, and having a HMR performed, it seems the event is now being called from multiple JS files:
click app.js:32:13
click app.e717bb1c3e94b3888f14.hot-update.js:31:13
And another change in the JS file causes yet another log, from another file on click:
click app.js:32:13
click app.e717bb1c3e94b3888f14.hot-update.js:31:13
click app.9bed096b8aae3ed59f54.hot-update.js:31:13
I’ve attempted using the “dispose” and “invalidate” methods provided by the HMR module, but must be doing something wrong as they consistently throw errors or fail to prevent this behavior.
Anyone have some insight on the correct setup to prevent these compounding events?
1 Like
This is happening because you have a const
button that is persisting between updates.
every update has a new anonymous event handler bound to the button. when you click all of the event handlers are still bound and still fire.
Three thoughts:
-
rather than self-accepting the module, i find it’s easier to create a component file and use the entrypoint to deal with HMR.
-
give the click handler a name so that you can reference it later during the accept
callback.
-
If you use React you have access to react-refresh
which handles a lot of this logic for you.
That said, here’s an example HMR implementation. I didn’t do a click handler, but the problem and solution are very similar (in this case I wind up with a ton of buttons rather than just updating the button I want to update unless I handle the old button ref):
import {makeButton} from './button'
const header = document.querySelector("header");
// Note this is `let`.
// we want to reassign during the accept callback
let button = makeButton();
header.appendChild(button);
/* When './button.js' is updated... */
import.meta.webpackHot?.accept('./button.js', (err) => {
/* ...remove the old ref */
header.removeChild(button)
/* ...update the button ref */
button = makeButton()
/* ...append the result */
header.appendChild(button);
/* If there was an error don't accept the update */
err && module.hot.invalidate()
});
../button.js
is really simple and kind of unimportant to the task, but for clarity:
export const makeButton = () => {
const el = document.createElement(`button`);
el.innerHTML = "click me!!";
el.classList.add(
"button",
"text-white",
"bg-blue-700",
"hover:bg-blue-800",
"font-medium",
"rounded-lg",
"text-sm",
"px-5",
"py-3",
"mr-2",
"mb-2",
);
return el;
};
I hope that’s helpful. Thank you for using bud.js and good luck with HMR!
3 Likes