@samitouri / QOS-React / commits / cdb6b4c554

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 () => {