Only hide outermost host nodes when Offscreen is hidden (#21250)
Brian Vaughn committed
Apr 19, 2021 at 21:33 UTC
cdb6b4c554bb40654d6c3e72f06b29003bec8dc7
3 files changed
+154
-34
packages/react-reconciler/src/ReactFiberCommitWork.new.js
+22
-17
@@ -1025,8 +1025,8 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1025
const current = finishedWork.alternate;
1026
const wasHidden = current !== null && current.memoizedState !== null;
1027
1028
- // Only hide the top-most host nodes.
1029
- let hiddenHostSubtreeRoot = null;
1028
+ // Only hide or unhide the top-most host nodes.
1029
+ let hostSubtreeRoot = null;
1030
1031
if (supportsMutation) {
1032
// We only have the top Fiber that was inserted but we need to recurse down its
@@ -1034,12 +1034,15 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1034
let node: Fiber = finishedWork;
1035
while (true) {
1036
if (node.tag === HostComponent) {
1037
- const instance = node.stateNode;
1038
- if (isHidden && hiddenHostSubtreeRoot === null) {
1039
- hiddenHostSubtreeRoot = node;
1040
- hideInstance(instance);
1041
- } else {
1042
- unhideInstance(node.stateNode, node.memoizedProps);
1037
+ if (hostSubtreeRoot === null) {
1038
+ hostSubtreeRoot = node;
1039
+
1040
+ const instance = node.stateNode;
1041
+ if (isHidden) {
1042
+ hideInstance(instance);
1043
+ } else {
1044
+ unhideInstance(node.stateNode, node.memoizedProps);
1045
+ }
1046
}
1047
1048
if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
@@ -1058,11 +1061,13 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1061
}
1062
}
1063
} else if (node.tag === HostText) {
1061
- const instance = node.stateNode;
1062
- if (isHidden && hiddenHostSubtreeRoot === null) {
1063
- hideTextInstance(instance);
1064
- } else {
1065
- unhideTextInstance(instance, node.memoizedProps);
1064
+ if (hostSubtreeRoot === null) {
1065
+ const instance = node.stateNode;
1066
+ if (isHidden) {
1067
+ hideTextInstance(instance);
1068
+ } else {
1069
+ unhideTextInstance(instance, node.memoizedProps);
1070
+ }
1071
}
1072
} else if (
1073
(node.tag === OffscreenComponent ||
@@ -1132,15 +1137,15 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1137
return;
1138
}
1139
1135
- if (hiddenHostSubtreeRoot === node) {
1136
- hiddenHostSubtreeRoot = null;
1140
+ if (hostSubtreeRoot === node) {
1141
+ hostSubtreeRoot = null;
1142
}
1143
1144
node = node.return;
1145
}
1146
1142
- if (hiddenHostSubtreeRoot === node) {
1143
- hiddenHostSubtreeRoot = null;
1147
+ if (hostSubtreeRoot === node) {
1148
+ hostSubtreeRoot = null;
1149
}
1150
1151
node.sibling.return = node.return;
packages/react-reconciler/src/ReactFiberCommitWork.old.js
+22
-17
@@ -1025,8 +1025,8 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1025
const current = finishedWork.alternate;
1026
const wasHidden = current !== null && current.memoizedState !== null;
1027
1028
- // Only hide the top-most host nodes.
1029
- let hiddenHostSubtreeRoot = null;
1028
+ // Only hide or unhide the top-most host nodes.
1029
+ let hostSubtreeRoot = null;
1030
1031
if (supportsMutation) {
1032
// We only have the top Fiber that was inserted but we need to recurse down its
@@ -1034,12 +1034,15 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1034
let node: Fiber = finishedWork;
1035
while (true) {
1036
if (node.tag === HostComponent) {
1037
- const instance = node.stateNode;
1038
- if (isHidden && hiddenHostSubtreeRoot === null) {
1039
- hiddenHostSubtreeRoot = node;
1040
- hideInstance(instance);
1041
- } else {
1042
- unhideInstance(node.stateNode, node.memoizedProps);
1037
+ if (hostSubtreeRoot === null) {
1038
+ hostSubtreeRoot = node;
1039
+
1040
+ const instance = node.stateNode;
1041
+ if (isHidden) {
1042
+ hideInstance(instance);
1043
+ } else {
1044
+ unhideInstance(node.stateNode, node.memoizedProps);
1045
+ }
1046
}
1047
1048
if (enableSuspenseLayoutEffectSemantics && isModernRoot) {
@@ -1058,11 +1061,13 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1061
}
1062
}
1063
} else if (node.tag === HostText) {
1061
- const instance = node.stateNode;
1062
- if (isHidden && hiddenHostSubtreeRoot === null) {
1063
- hideTextInstance(instance);
1064
- } else {
1065
- unhideTextInstance(instance, node.memoizedProps);
1064
+ if (hostSubtreeRoot === null) {
1065
+ const instance = node.stateNode;
1066
+ if (isHidden) {
1067
+ hideTextInstance(instance);
1068
+ } else {
1069
+ unhideTextInstance(instance, node.memoizedProps);
1070
+ }
1071
}
1072
} else if (
1073
(node.tag === OffscreenComponent ||
@@ -1132,15 +1137,15 @@ function hideOrUnhideAllChildren(finishedWork, isHidden) {
1137
return;
1138
}
1139
1135
- if (hiddenHostSubtreeRoot === node) {
1136
- hiddenHostSubtreeRoot = null;
1140
+ if (hostSubtreeRoot === node) {
1141
+ hostSubtreeRoot = null;
1142
}
1143
1144
node = node.return;
1145
}
1146
1142
- if (hiddenHostSubtreeRoot === node) {
1143
- hiddenHostSubtreeRoot = null;
1147
+ if (hostSubtreeRoot === node) {
1148
+ hostSubtreeRoot = null;
1149
}
1150
1151
node.sibling.return = node.return;
packages/react-reconciler/src/__tests__/ReactSuspenseEffectsSemantics-test.js
+110
@@ -1284,6 +1284,116 @@ describe('ReactSuspenseEffectsSemantics', () => {
1284
]);
1285
});
1286
1287
+ // @gate enableSuspenseLayoutEffectSemantics
1288
+ // @gate enableCache
1289
+ it('should show nested host nodes if multiple boundaries resolve at the same time', async () => {
1290
+ function App({innerChildren = null, outerChildren = null}) {
1291
+ return (
1292
+ <Suspense fallback={<Text text="OuterFallback" />}>
1293
+ <Text text="Outer" />
1294
+ {outerChildren}
1295
+ <Suspense fallback={<Text text="InnerFallback" />}>
1296
+ <Text text="Inner" />
1297
+ {innerChildren}
1298
+ </Suspense>
1299
+ </Suspense>
1300
+ );
1301
+ }
1302
+
1303
+ // Mount
1304
+ await ReactNoop.act(async () => {
1305
+ ReactNoop.render(<App />);
1306
+ });
1307
+ expect(Scheduler).toHaveYielded([
1308
+ 'Text:Outer render',
1309
+ 'Text:Inner render',
1310
+ 'Text:Outer create layout',
1311
+ 'Text:Inner create layout',
1312
+ 'Text:Outer create passive',
1313
+ 'Text:Inner create passive',
1314
+ ]);
1315
+ expect(ReactNoop.getChildren()).toEqual([span('Outer'), span('Inner')]);
1316
+
1317
+ // Suspend the inner Suspense subtree (only inner effects should be destroyed)
1318
+ ReactNoop.act(() => {
1319
+ ReactNoop.render(
1320
+ <App innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />} />,
1321
+ );
1322
+ });
1323
+ await advanceTimers(1000);
1324
+ expect(Scheduler).toHaveYielded([
1325
+ 'Text:Outer render',
1326
+ 'Text:Inner render',
1327
+ 'Suspend:InnerAsync_1',
1328
+ 'Text:InnerFallback render',
1329
+ 'Text:Inner destroy layout',
1330
+ 'Text:InnerFallback create layout',
1331
+ ]);
1332
+ expect(Scheduler).toFlushAndYield(['Text:InnerFallback create passive']);
1333
+ expect(ReactNoop.getChildren()).toEqual([
1334
+ span('Outer'),
1335
+ spanHidden('Inner'),
1336
+ span('InnerFallback'),
1337
+ ]);
1338
+
1339
+ // Suspend the outer Suspense subtree (outer effects and inner fallback effects should be destroyed)
1340
+ // (This check also ensures we don't destroy effects for mounted inner fallback.)
1341
+ ReactNoop.act(() => {
1342
+ ReactNoop.render(
1343
+ <App
1344
+ outerChildren={<AsyncText text="OuterAsync_1" ms={1000} />}
1345
+ innerChildren={<AsyncText text="InnerAsync_1" ms={1000} />}
1346
+ />,
1347
+ );
1348
+ });
1349
+ await advanceTimers(1000);
1350
+ expect(Scheduler).toHaveYielded([
1351
+ 'Text:Outer render',
1352
+ 'Suspend:OuterAsync_1',
1353
+ 'Text:Inner render',
1354
+ 'Suspend:InnerAsync_1',
1355
+ 'Text:InnerFallback render',
1356
+ 'Text:OuterFallback render',
1357
+ 'Text:Outer destroy layout',
1358
+ 'Text:InnerFallback destroy layout',
1359
+ 'Text:OuterFallback create layout',
1360
+ ]);
1361
+ expect(Scheduler).toFlushAndYield(['Text:OuterFallback create passive']);
1362
+ expect(ReactNoop.getChildren()).toEqual([
1363
+ spanHidden('Outer'),
1364
+ spanHidden('Inner'),
1365
+ spanHidden('InnerFallback'),
1366
+ span('OuterFallback'),
1367
+ ]);
1368
+
1369
+ // Resolve both suspended trees.
1370
+ await ReactNoop.act(async () => {
1371
+ await resolveText('OuterAsync_1');
1372
+ await resolveText('InnerAsync_1');
1373
+ });
1374
+ expect(Scheduler).toHaveYielded([
1375
+ 'Text:Outer render',
1376
+ 'AsyncText:OuterAsync_1 render',
1377
+ 'Text:Inner render',
1378
+ 'AsyncText:InnerAsync_1 render',
1379
+ 'Text:OuterFallback destroy layout',
1380
+ 'Text:Outer create layout',
1381
+ 'AsyncText:OuterAsync_1 create layout',
1382
+ 'Text:Inner create layout',
1383
+ 'AsyncText:InnerAsync_1 create layout',
1384
+ 'Text:OuterFallback destroy passive',
1385
+ 'Text:InnerFallback destroy passive',
1386
+ 'AsyncText:OuterAsync_1 create passive',
1387
+ 'AsyncText:InnerAsync_1 create passive',
1388
+ ]);
1389
+ expect(ReactNoop.getChildren()).toEqual([
1390
+ span('Outer'),
1391
+ span('OuterAsync_1'),
1392
+ span('Inner'),
1393
+ span('InnerAsync_1'),
1394
+ ]);
1395
+ });
1396
+
1397
// @gate enableSuspenseLayoutEffectSemantics
1398
// @gate enableCache
1399
it('should be cleaned up inside of a fallback that suspends', async () => {