[DOM] Fix Fragment removeEventListener dropping tracked listeners (#37160)
Removing an unregistered listener used splice(-1), which deleted the last tracked entry and left the real handler stuck on DOM children.
Jack Pope committed
Aug 11, 2026 at 17:50 UTC
3cba19c97725975c5a7d0d9aba00d97553158654
2 files changed
+57
-17
packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js
+16
-17
@@ -3073,24 +3073,23 @@ FragmentInstance.prototype.removeEventListener = function (
3073
if (listeners === null) {
3074
return;
3075
}
3076
- if (typeof listeners !== 'undefined' && listeners.length > 0) {
3077
- traverseFragmentInstancesAndTextInstances(
3078
- this._fragmentFiber,
3079
- removeEventListenerFromChild,
3080
- type,
3081
- listener,
3082
- optionsOrUseCapture,
3083
- );
3084
- const index = indexOfEventListener(
3085
- listeners,
3086
- type,
3087
- listener,
3088
- optionsOrUseCapture,
3089
- );
3090
- if (this._eventListeners !== null) {
3091
- this._eventListeners.splice(index, 1);
3092
- }
3076
+ const index = indexOfEventListener(
3077
+ listeners,
3078
+ type,
3079
+ listener,
3080
+ optionsOrUseCapture,
3081
+ );
3082
+ if (index === -1) {
3083
+ return;
3084
}
3085
+ traverseFragmentInstancesAndTextInstances(
3086
+ this._fragmentFiber,
3087
+ removeEventListenerFromChild,
3088
+ type,
3089
+ listener,
3090
+ optionsOrUseCapture,
3091
+ );
3092
+ listeners.splice(index, 1);
3093
};
3094
function removeEventListenerFromChild(
3095
child: Fiber,
packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
+41
@@ -606,6 +606,47 @@ describe('FragmentRefs', () => {
606
expect(logs).toEqual(['B']);
607
});
608
609
+ // @gate enableFragmentRefs
610
+ it('regression: does not detach a registered listener when removing an unregistered one', async () => {
611
+ const fragmentRef = React.createRef();
612
+ const childRef = React.createRef();
613
+ const root = ReactDOMClient.createRoot(container);
614
+ let logs = [];
615
+
616
+ function registeredListener() {
617
+ logs.push('registered');
618
+ }
619
+
620
+ function unregisteredListener() {
621
+ logs.push('unregistered');
622
+ }
623
+
624
+ await act(() => {
625
+ root.render(
626
+ <Fragment ref={fragmentRef}>
627
+ <div ref={childRef}>child</div>
628
+ </Fragment>,
629
+ );
630
+ });
631
+
632
+ fragmentRef.current.addEventListener('click', registeredListener);
633
+ childRef.current.click();
634
+ expect(logs).toEqual(['registered']);
635
+
636
+ // Regression: removing a listener that was never added must be a no-op.
637
+ // It must not detach registered listeners from fragmentInstance,
638
+ // causing them to stay attached to DOM even after removeEventListener.
639
+ fragmentRef.current.removeEventListener('click', unregisteredListener);
640
+ logs = [];
641
+ childRef.current.click();
642
+ expect(logs).toEqual(['registered']);
643
+
644
+ fragmentRef.current.removeEventListener('click', registeredListener);
645
+ logs = [];
646
+ childRef.current.click();
647
+ expect(logs).toEqual([]);
648
+ });
649
+
650
// @gate enableFragmentRefs
651
it('adds and removes event listeners from children with multiple fragments', async () => {
652
const fragmentRef = React.createRef();