diff --git a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
index 86c1021db65..ab3aaf26209 100644
--- a/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMFragmentRefs-test.js
@@ -852,6 +852,60 @@ describe('FragmentRefs', () => {
expect(logs).toEqual(['fragment']);
});
+ // @gate enableFragmentRefs && enableFragmentRefsTextNodes
+ it('removes event listeners from a deleted text child', async () => {
+ const fragmentRef = React.createRef();
+ const parentRef = React.createRef();
+ const root = ReactDOMClient.createRoot(container);
+ let hideText;
+
+ function Component() {
+ const [shouldShowText, setShouldShowText] = React.useState(true);
+ hideText = () => {
+ setShouldShowText(false);
+ };
+
+ return (
+
+
+ {shouldShowText ? 'Hello' : null}
+
+
+ );
+ }
+
+ await act(() => {
+ root.render();
+ });
+
+ const textNode = Array.from(parentRef.current.childNodes).find(
+ node => node.nodeType === 3,
+ );
+ expect(textNode).not.toBe(undefined);
+
+ const logs = [];
+ fragmentRef.current.addEventListener('click', () => {
+ logs.push('fragment');
+ });
+
+ textNode.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ expect(logs).toEqual(['fragment']);
+
+ await act(() => {
+ hideText();
+ });
+
+ const detachedHost = document.createElement('div');
+ document.body.appendChild(detachedHost);
+ detachedHost.appendChild(textNode);
+
+ logs.length = 0;
+ textNode.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ expect(logs).toEqual([]);
+
+ document.body.removeChild(detachedHost);
+ });
+
// @gate enableFragmentRefs
it('applies event listeners to host children nested within non-host children', async () => {
const fragmentRef = React.createRef();
@@ -1250,6 +1304,66 @@ describe('FragmentRefs', () => {
// Event order is flipped here because the nested child re-registers first
expect(logs).toEqual(['clicked 2', 'clicked 1']);
});
+
+ // @gate enableFragmentRefs && enableFragmentRefsTextNodes
+ it('does not dispatch fragment events from text children while hidden', async () => {
+ const parentRef = React.createRef();
+ const fragmentRef = React.createRef();
+ const root = ReactDOMClient.createRoot(container);
+
+ function Test({mode}) {
+ return (
+
+
+
+ Element
+ Text
+
+
+
+ );
+ }
+
+ await act(() => {
+ root.render();
+ });
+
+ const logs = [];
+ fragmentRef.current.addEventListener('click', e => {
+ logs.push(
+ e.target.nodeType === 3
+ ? 'text'
+ : e.target.id || e.target.tagName,
+ );
+ });
+
+ const textNode = Array.from(parentRef.current.childNodes).find(
+ node => node.nodeType === 3,
+ );
+ expect(textNode).not.toBe(undefined);
+
+ document.getElementById('child').click();
+ textNode.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ expect(logs).toEqual(['child', 'text']);
+
+ logs.length = 0;
+ await act(() => {
+ root.render();
+ });
+
+ document.getElementById('child').click();
+ textNode.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ expect(logs).toEqual([]);
+
+ logs.length = 0;
+ await act(() => {
+ root.render();
+ });
+
+ document.getElementById('child').click();
+ textNode.dispatchEvent(new MouseEvent('click', {bubbles: true}));
+ expect(logs).toEqual(['child', 'text']);
+ });
});
});
diff --git a/packages/react-reconciler/src/ReactFiberCommitWork.js b/packages/react-reconciler/src/ReactFiberCommitWork.js
index 415fecf8713..3ccba8a847a 100644
--- a/packages/react-reconciler/src/ReactFiberCommitWork.js
+++ b/packages/react-reconciler/src/ReactFiberCommitWork.js
@@ -1570,16 +1570,20 @@ function commitDeletionEffectsOnFiber(
if (!offscreenSubtreeWasHidden) {
safelyDetachRef(deletedFiber, nearestMountedAncestor);
}
- if (
- enableFragmentRefs &&
- (deletedFiber.tag === HostComponent ||
- (enableFragmentRefsTextNodes && deletedFiber.tag === HostText))
- ) {
+ if (enableFragmentRefs) {
commitFragmentInstanceDeletionEffects(deletedFiber);
}
// Intentional fallthrough to next branch
}
case HostText: {
+ if (
+ enableFragmentRefs &&
+ enableFragmentRefsTextNodes &&
+ // HostComponent falls through into this case.
+ deletedFiber.tag === HostText
+ ) {
+ commitFragmentInstanceDeletionEffects(deletedFiber);
+ }
// We only need to remove the nearest host child. Set the host parent
// to `null` on the stack to indicate that nested children don't
// need to be removed.
@@ -3152,9 +3156,10 @@ function disappearLayoutEffects(
if (
enableFragmentRefs &&
+ // HostHoistable shares this case via fallthrough but must not be
+ // attributed to fragment instances. HostText has its own case below.
(finishedWork.tag === HostComponent ||
- finishedWork.tag === HostSingleton ||
- (enableFragmentRefsTextNodes && finishedWork.tag === HostText))
+ finishedWork.tag === HostSingleton)
) {
commitFragmentInstanceDeletionEffects(finishedWork);
}
@@ -3165,6 +3170,12 @@ function disappearLayoutEffects(
);
break;
}
+ case HostText: {
+ if (enableFragmentRefs && enableFragmentRefsTextNodes) {
+ commitFragmentInstanceDeletionEffects(finishedWork);
+ }
+ break;
+ }
case HostHoistable: {
// TODO (Offscreen) Check: flags & RefStatic
safelyDetachRef(finishedWork, finishedWork.return);
@@ -3389,6 +3400,12 @@ function reappearLayoutEffects(
safelyAttachRef(finishedWork, finishedWork.return);
break;
}
+ case HostText: {
+ if (enableFragmentRefs && enableFragmentRefsTextNodes) {
+ commitFragmentInstanceInsertionEffects(finishedWork);
+ }
+ break;
+ }
case HostHoistable: {
// $FlowFixMe[constant-condition]
if (supportsResources) {