hotwired / hotwired/stimulus

disconnect() not called if outlet is accessed in connect()

Open
#763 7 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

Dominant language
TypeScript
Stars
13.1k
Forks
441
PR merge metrics
No merged PRs in 30d

Description

I noticed that accessing an outlet in connect() causes the reference count of that controller to be 2 instead of 1 after start has finished. That means all actions which under normal circumstances cause the controller to be disconnected (like removing the data-controller attribute) don't cause the disconnect anymore.

The following code snippet illustrates the problem

<!doctype html>
<html>
<head>
  <meta charset="utf-8">
  <script type="module">
    import { Application, Controller } from "https://unpkg.com/@hotwired/stimulus/dist/stimulus.js"
    window.Stimulus = Application.start()

    Stimulus.register("child", class extends Controller {
      connect() {
        console.log("Child connected")
      }
      disconnect() {
        console.log("Child disconnected")
        alert("works as expected")
      }
    })

    Stimulus.register("parent", class extends Controller {
      static outlets = [ "child" ]
  
      connect() {
        console.log("Parent connected")
        console.log(this.childOutlet) // <- works without this line 💥
      }
      disconnect() {
        console.log("Parent disconnected")
      }
      
      listChildren() {
        console.log(this.childOutlet)
      }
    });

    document.getElementById("remove").addEventListener("click", function() {
      console.log("Removing data-controller attribute");
      document.getElementById("child").setAttribute("data-controller", "");
    });
  </script>
</head>
<body>
  <div id="parent" data-controller="parent" data-parent-child-outlet="#child">
    <button data-action="click->parent#listChildren">List children</button>
  </div>
  <div id="child" data-controller="child"></div>
  <button id="remove">Remove data-controller=child attribute</button>
</body>
</html>

Setting a breakpoint in ScopeObserver.elementMatchedValue() and reloading the page you can see that it is hit twice for child. Clicking the remove button does not trigger disconnect() to be called. Everything works as expected with the marked line commented out.

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

Reproduce the HTML example and trace the outlet lookup from connect() through ScopeObserver.elementMatchedValue(), focusing on why the child is matched twice. Verify the fix by confirming that removing the child data-controller attribute calls disconnect() once and that normal outlet access remains functional.

Written by the indexing model from the issue text.

Assessment

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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.