facebook / react · Issue No. 37598
Fragment refs keep the caller-owned listener options object and read it again later. Changing that object after registration can leave an event listener active after removeEventListener, once delivery or signal abort.
Current react/react main: 019019be403c3269e15b8d7ebefb57d30f84086b (React source version 19.3.0). Verified on source, not inferred from an older published release.
The snippet assumes React/createPortal, createRoot and flushSync imports, and a mounted root created with createRoot(container).
const ref = React.createRef();
// Mount with flushSync so ref.current is available:
flushSync(() => root.render(<React.Fragment ref={ref}><button /></React.Fragment>));
const options = {capture: false};
let calls = 0;
const listener = () => calls++;
ref.current.addEventListener('click', listener, options);
options.capture = true;
ref.current.removeEventListener('click', listener, false);
container.firstChild.click();
console.log(calls); // 1, expected 0
A runnable repository regression test is included in the linked fix PR.
The listener remains active. A once listener may also fire again on another child, and abort cleanup can remove the registration record while leaving the original native listener attached.
Registration should keep the capture/options values read at addEventListener time, as the native API does. Removing the original non-capture listener should stop its callbacks.
StoredEventListener.optionsOrUseCapture aliases the caller object. indexOfEventListener and cleanup derive capture from the mutated object, although existing native listeners were attached using the original capture value.
Source: packages/react-dom-bindings/src/client/ReactFiberConfigDOM.js.
Severity assessment: Medium. Fragment refs keep the caller-owned listener options object and read it again later. Changing that object after registration can leave an event listener active after removeEventListener, once delivery or signal abort.
Before: removal/later-child and once regressions each produce 2 calls instead of 0 and 1 respectively. After: both development and production Fragment suites pass 101/101 tests, including the new abort regression. ESLint, Prettier and Flow (dom-node-webpack configuration, which includes DOM bindings) pass.
Searched all Issue/PR states for Fragment capture/options/mutated/snapshot and examined recent Fragment commits. #36047 and #37251 normalize equivalent option forms; #37457 handles aborted registration cleanup. They do not snapshot caller-owned options, and the new regressions fail on main after those fixes. No equivalent issue or PR was found.
Boolean and omitted options retain their existing behavior. Snapshotting reads the supported dictionary fields once and preserves optional passive defaults. No public API is added.
Relay reads this issue against the repository's contribution signals: the files it is likely to touch, how the maintainers triage work this size, and what the first contribution would exercise.
The full analysis for this issue is still being assembled. Until then, the description above and the thread on GitHub are the most reliable context.