Skip to content

Reusing a bound event handler tuple breaks non-delegated events #3186

Description

@nickshiro

Describe the bug

Reusing the same [handler, data] tuple on multiple elements breaks non-delegated on* events such as onScroll. The first element receives the correct event. On the second element, the handler receives the bound data in place of the Event.
This happens because the direct-listener path replaces handler[0] with an element-specific wrapper, mutating the user-provided tuple. When that tuple is reused, Solid wraps the already wrapped function again. The delegated event path, such as onClick, does not appear to be affected.

Steps to Reproduce the Bug or Issue

import { render } from "solid-js/web";
import type { JSX } from "solid-js";

let second!: HTMLDivElement;

const handler: JSX.EventHandlerUnion<HTMLDivElement, Event> = [
  (data, event) => {
    console.log({
      data,
      event,
      currentTarget: event.currentTarget
    });
  },
  "shared"
];

function App() {
  return (
    <>
      <div onScroll={handler}>First</div>
      <div ref={second} onScroll={handler}>
        Second
      </div>

      <button
        onClick={() => {
          second.dispatchEvent(new Event("scroll"));
        }}
      >
        Dispatch scroll
      </button>
    </>
  );
}

render(() => <App />, document.getElementById("app")!);

Click the button to dispatch scroll on the second element.

A regression test demonstrating the issue:

  test("the same handler tuple can be reused on multiple elements", () => {
    const calls: Array<[string, HTMLDivElement, Event]> = [];
    let second!: HTMLDivElement;

    const handler: JSX.EventHandlerUnion<HTMLDivElement, Event> = [
      (data, event) => calls.push([data, event.currentTarget, event]),
      "shared"
    ];

    createRoot(() => (
      <>
        <div onScroll={handler} />
        <div ref={second} onScroll={handler} />
      </>
    ));

    const event = new Event("scroll");
    second.dispatchEvent(event);

    expect(calls).toEqual([["shared", second, event]]);
  });

The assertion currently fails because the received values are effectively:

  [["shared", undefined, "shared"]]

Expected behavior

The tuple should remain unchanged and be reusable across elements.

The handler installed on the second element should receive:

  1. "shared" as the bound data.
  2. The dispatched Event.
  3. The second element as event.currentTarget.

Solid should not mutate the user-provided handler tuple while installing the listener.

Platform

  • Solid: reproduced with 1.9.15 and next at f051db6.
  • Test environment: Vitest with jsdom.
  • OS: Linux.

Additional context

The mutation happens in the non-delegated array-handler branch:

  const handlerFn = handler[0];
  node.addEventListener(
    name,
    (handler[0] = e => handlerFn.call(node, handler[1], e))
  );

The wrapper likely needs to be stored separately for each element and event instead of replacing handler[0].

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions