interact.js icon indicating copy to clipboard operation
interact.js copied to clipboard

Memory leak after unsetting interact.js

Open nesk opened this issue 1 month ago • 0 comments

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() and removeDocument() (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>

nesk avatar Nov 25 '25 16:11 nesk