Skip to content

Hooks with dynamic ID are not properly destroyed #3651

@SteffenDE

Description

@SteffenDE

Originally reported by @Gazler in Discord:

Mix.install([
  {:phoenix_playground, "~> 0.1.6"},
  {:phoenix_live_view, "~> 1.0.0", override: true},
])

defmodule DemoLive do
  use Phoenix.LiveView

  def mount(_params, _session, socket) do
    if connected?(socket) do
      send(self(), :change_id)
    end
    socket =
      socket
      |> assign(id: 1, counter: 0)
      |> push_event("myevent", %{})
    {:ok, socket}
  end

  def handle_info(:change_id, socket) do
    {:noreply, assign(socket, id: 2)}
  end

  def handle_event("lol", _params, socket) do
    {:noreply, socket}
  end

  def handle_event("reload", _params, socket) do
    counter = socket.assigns.counter + 1
    socket =
      socket
      |> push_event("myevent", %{})
      |> assign(counter: counter)

    socket =
      if counter > 4096 do
        push_navigate(socket, to: "/")
      else
        socket
      end

    {:noreply, socket}
  end

  def render(assigns) do
    ~H"""
    <div id="main" phx-hook="OuterHook">
      <div phx-hook="InnerHook" id={"id-#{@id}"} />
      This is an example of nested hooks resulting in a "ghost" element
      that isn't on the DOM, and is never cleaned up. In this specific example
      a timeout is used to show how the number of events being sent to the server
      grows exponentially.
      <p>Doing any of the following things fixes it:</p>
      <ol>
        <li>Setting the `phx-hook` to use a fixed id.</li>
        <li>Removing the `pushEvent` from the OuterHook `mounted` callback.</li>
        <li>Deferring the pushEvent by wrapping it in a setTimeout.</li>
      </ol>
    </div>
    <div>
      To prevent blowing up your computer, the page will reload after 4096 events, which takes ~12 seconds
    </div>
    <div style="color: blue; font-size: 20px" id="counter">Total Event Calls: {@counter}</div>
    <div style="color: red; font-size: 72px" id="notice" phx-update="ignore">I will disappear if the bug is not present.</div>
    <script>
      window.hooks.OuterHook = {
        mounted() {
          this.pushEvent("lol")
        },
      }
      window.hooks.InnerHook = {
        mounted() {
          console.log("MOUNTED", this.el);
          this.handleEvent('myevent', this._handleEvent(this));
        },
        destroyed() {
          document.getElementById("notice").innerHTML = "";
          console.log("DESTROYED", this.el);
        },
        _handleEvent(self) {
          return () => {
            setTimeout(() => {
              console.warn("reloading", self.el);
              self.pushEvent("reload", {})
            }, 1000)
          }
        }
      }
    </script>
    """
  end
end

PhoenixPlayground.start(live: DemoLive, port: 4200)

The hook element has a dynamic ID, but when it changes, the old hook is not cleared, causing events to exponentially increase.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions