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:
- "shared" as the bound data.
- The dispatched Event.
- 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].
Describe the bug
Reusing the same
[handler, data]tuple on multiple elements breaks non-delegatedon*events such asonScroll. The first element receives the correct event. On the second element, the handler receives the bound data in place of theEvent.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 asonClick, does not appear to be affected.Steps to Reproduce the Bug or Issue
Click the button to dispatch scroll on the second element.
A regression test demonstrating the issue:
The assertion currently fails because the received values are effectively:
Expected behavior
The tuple should remain unchanged and be reusable across elements.
The handler installed on the second element should receive:
Solid should not mutate the user-provided handler tuple while installing the listener.
Platform
Additional context
The mutation happens in the non-delegated array-handler branch:
The wrapper likely needs to be stored separately for each element and event instead of replacing handler[0].