Skip to content

Memory leak after unsetting interact.js #1069

Description

@nesk

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/interactjs@1.10.27/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>

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions