interact.js
interact.js copied to clipboard
Memory leak after unsetting interact.js
Calling interactInstance.unset() doesn't seem to free all the references to the element.
I've got a vanilla reproducible example where:
- A checkbox allows to mount/unmount a
<div>Drag Me!</div> - When mounted, interact.js is instanciated
- When unmounted, interact.js is unloaded using
unset()andremoveDocument()(despite not being sure what the latter does)
Now, here's how you can reproduce:
- Open the example in your browser (code below)
- Check the box, the
<div>Drag Me!</div>is mounted - interact.js is automatically instanciated
- Drag the box around (you must do this)
- Uncheck the box, the div is unmounted
- interact.js is automatically unloaded
- Now open the Chrome DevTools on the Memory tab and take a Detached Elements snapshot
Actual: the div is listed as detached Expected: the div shouldn't exist anymore
Looking at the various retainers in the Heap snapshot, we can see the div is still referenced by the following object:
window.interact.scope.interactions.list[0].downPointer.__set.target // Returns the relevant div
And here's the example code:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Draggable Div with interact.js</title>
<script src="https://cdn.jsdelivr.net/npm/[email protected]/dist/interact.js"></script>
</head>
<body>
<div class="controls">
<label>
<input type="checkbox" id="showBoxCheckbox">
Show Draggable Box
</label>
</div>
<div id="boxContainer"></div>
<script>
class DraggableBox {
constructor(container) {
this.container = container;
this.element = null;
this.interactInstance = null;
this.position = { x: 0, y: 0 };
}
mount() {
// Create element
this.element = document.createElement('div');
this.element.className = 'draggable-box draggable';
this.element.textContent = 'Drag Me!';
this.container.appendChild(this.element);
// Initialize interact.js
console.log("INTERACT INIT");
this.interactInstance = interact(this.element)
.draggable({
listeners: {
move: (event) => this.dragMoveListener(event)
}
});
}
dragMoveListener(event) {
this.position.x += event.dx;
this.position.y += event.dy;
event.target.style.transform = `translate(${this.position.x}px, ${this.position.y}px)`;
}
unmount() {
// Cleanup interact.js
if (this.interactInstance) {
console.log("INTERACT UNSET");
this.interactInstance.unset();
this.interactInstance = null;
interact.removeDocument(document);
}
// Remove element from DOM
if (this.element && this.element.parentNode) {
this.element.parentNode.removeChild(this.element);
}
this.element = null;
}
}
// Main App Logic
const checkbox = document.getElementById('showBoxCheckbox');
const container = document.getElementById('boxContainer');
let draggableBox = null;
checkbox.addEventListener('change', function() {
if (this.checked) {
// Mount component
draggableBox = new DraggableBox(container);
draggableBox.mount();
} else {
// Unmount component
if (draggableBox) {
draggableBox.unmount();
draggableBox = null;
}
}
});
</script>
</body>
</html>