diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
index b6feee098b4f4..9416ac45dd4c5 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.new.js
@@ -63,6 +63,7 @@ import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
import {
Ref,
RefStatic,
+ Placement,
Update,
Visibility,
NoFlags,
@@ -1369,13 +1370,26 @@ function completeWork(
}
}
- // Don't bubble properties for hidden children.
- if (
- !nextIsHidden ||
- includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane)) ||
- (workInProgress.mode & ConcurrentMode) === NoMode
- ) {
+ if (!nextIsHidden || (workInProgress.mode & ConcurrentMode) === NoMode) {
bubbleProperties(workInProgress);
+ } else {
+ // Don't bubble properties for hidden children unless we're rendering
+ // at offscreen priority.
+ if (includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane))) {
+ bubbleProperties(workInProgress);
+ if (supportsMutation) {
+ // Check if there was an insertion or update in the hidden subtree.
+ // If so, we need to hide those nodes in the commit phase, so
+ // schedule a visibility effect.
+ if (
+ workInProgress.tag !== LegacyHiddenComponent &&
+ workInProgress.subtreeFlags & (Placement | Update) &&
+ newProps.mode !== 'unstable-defer-without-hiding'
+ ) {
+ workInProgress.flags |= Visibility;
+ }
+ }
+ }
}
if (enableCache) {
diff --git a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
index 612ad2db52c9f..35d17f871ab93 100644
--- a/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
+++ b/packages/react-reconciler/src/ReactFiberCompleteWork.old.js
@@ -63,6 +63,7 @@ import {NoMode, ConcurrentMode, ProfileMode} from './ReactTypeOfMode';
import {
Ref,
RefStatic,
+ Placement,
Update,
Visibility,
NoFlags,
@@ -1369,13 +1370,26 @@ function completeWork(
}
}
- // Don't bubble properties for hidden children.
- if (
- !nextIsHidden ||
- includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane)) ||
- (workInProgress.mode & ConcurrentMode) === NoMode
- ) {
+ if (!nextIsHidden || (workInProgress.mode & ConcurrentMode) === NoMode) {
bubbleProperties(workInProgress);
+ } else {
+ // Don't bubble properties for hidden children unless we're rendering
+ // at offscreen priority.
+ if (includesSomeLane(subtreeRenderLanes, (OffscreenLane: Lane))) {
+ bubbleProperties(workInProgress);
+ if (supportsMutation) {
+ // Check if there was an insertion or update in the hidden subtree.
+ // If so, we need to hide those nodes in the commit phase, so
+ // schedule a visibility effect.
+ if (
+ workInProgress.tag !== LegacyHiddenComponent &&
+ workInProgress.subtreeFlags & (Placement | Update) &&
+ newProps.mode !== 'unstable-defer-without-hiding'
+ ) {
+ workInProgress.flags |= Visibility;
+ }
+ }
+ }
}
if (enableCache) {
diff --git a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
index 9f9225fea3bad..4dfd6459a33a3 100644
--- a/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+++ b/packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
@@ -201,14 +201,7 @@ describe('ReactOffscreen', () => {
});
// No layout effect.
expect(Scheduler).toHaveYielded(['Child']);
- if (gate(flags => flags.persistent)) {
- expect(root).toMatchRenderedOutput();
- } else {
- // TODO: Offscreen does not yet hide/unhide children correctly in mutation
- // mode. Until we do, it should only be used inside a host component
- // wrapper whose visibility is toggled simultaneously.
- expect(root).toMatchRenderedOutput();
- }
+ expect(root).toMatchRenderedOutput();
// Unhide the tree. The layout effect is mounted.
await act(async () => {
@@ -255,14 +248,7 @@ describe('ReactOffscreen', () => {
);
});
expect(Scheduler).toHaveYielded(['Unmount layout', 'Child']);
- if (gate(flags => flags.persistent)) {
- expect(root).toMatchRenderedOutput();
- } else {
- // TODO: Offscreen does not yet hide/unhide children correctly in mutation
- // mode. Until we do, it should only be used inside a host component
- // wrapper whose visibility is toggled simultaneously.
- expect(root).toMatchRenderedOutput();
- }
+ expect(root).toMatchRenderedOutput();
// Unhide the tree. The layout effect is re-mounted.
await act(async () => {
@@ -299,14 +285,7 @@ describe('ReactOffscreen', () => {
);
});
expect(Scheduler).toHaveYielded(['Child']);
- if (gate(flags => flags.persistent)) {
- expect(root).toMatchRenderedOutput();
- } else {
- // TODO: Offscreen does not yet hide/unhide children correctly in mutation
- // mode. Until we do, it should only be used inside a host component
- // wrapper whose visibility is toggled simultaneously.
- expect(root).toMatchRenderedOutput();
- }
+ expect(root).toMatchRenderedOutput();
// Show the tree. The layout effect is mounted.
await act(async () => {
@@ -328,14 +307,7 @@ describe('ReactOffscreen', () => {
);
});
expect(Scheduler).toHaveYielded(['Unmount layout', 'Child']);
- if (gate(flags => flags.persistent)) {
- expect(root).toMatchRenderedOutput();
- } else {
- // TODO: Offscreen does not yet hide/unhide children correctly in mutation
- // mode. Until we do, it should only be used inside a host component
- // wrapper whose visibility is toggled simultaneously.
- expect(root).toMatchRenderedOutput();
- }
+ expect(root).toMatchRenderedOutput();
});
// @gate experimental || www
@@ -385,4 +357,80 @@ describe('ReactOffscreen', () => {
});
expect(Scheduler).toHaveYielded(['Unmount layout']);
});
+
+ // @gate experimental || www
+ it('hides new insertions into an already hidden tree', async () => {
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render(
+
+ Hi
+ ,
+ );
+ });
+ expect(root).toMatchRenderedOutput(Hi);
+
+ // Insert a new node into the hidden tree
+ await act(async () => {
+ root.render(
+
+ Hi
+ Something new
+ ,
+ );
+ });
+ expect(root).toMatchRenderedOutput(
+ <>
+ Hi
+ {/* This new node should also be hidden */}
+ Something new
+ >,
+ );
+ });
+
+ // @gate experimental || www
+ it('hides updated nodes inside an already hidden tree', async () => {
+ const root = ReactNoop.createRoot();
+ await act(async () => {
+ root.render(
+
+ Hi
+ ,
+ );
+ });
+ expect(root).toMatchRenderedOutput(Hi);
+
+ // Set the `hidden` prop to on an already hidden node
+ await act(async () => {
+ root.render(
+
+ Hi
+ ,
+ );
+ });
+ // It should still be hidden, because the Offscreen container overrides it
+ expect(root).toMatchRenderedOutput(Hi);
+
+ // Unhide the boundary
+ await act(async () => {
+ root.render(
+
+ Hi
+ ,
+ );
+ });
+ // It should still be hidden, because of the prop
+ expect(root).toMatchRenderedOutput(Hi);
+
+ // Remove the `hidden` prop
+ await act(async () => {
+ root.render(
+
+ Hi
+ ,
+ );
+ });
+ // Now it's visible
+ expect(root).toMatchRenderedOutput(Hi);
+ });
});