taye / taye/interact.js

Memory leak after unsetting interact.js

Open
#1,069 1 comment 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
12.9k
Forks
804
PR merge metrics
No merged PRs in 30d

Description

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>

Contributor guide

No contributing guide indexed for this repository

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start with the provided vanilla reproduction and inspect cleanup around interactInstance.unset(), interact.removeDocument(document), and window.interact.scope.interactions.list[0].downPointer.__set.target. Use Chrome DevTools Memory detached-elements snapshots after dragging and unmounting; done means the removed div is no longer retained.

Written by the indexing model from the issue text.

Assessment

Tech stack
javascript, typescript
Domain
frontend, performance
Issue type
Bug
Difficulty
4/5
Estimated time
3-5 days
Activity status
Stale
Clarity
Mostly clear
Newbie friendliness
42/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.