Bail out of updates in offscreen trees (#15666)
* Bail out of updates in offscreen trees * Address review
Dan Abramov committed
May 16, 2019 at 11:12 UTC
bb89b4eacc4edc0954910955322296789d8e2089
3 files changed
+83
-2
packages/react-dom/src/__tests__/ReactUpdates-test.js
+72
@@ -1290,6 +1290,78 @@ describe('ReactUpdates', () => {
1290
expect(ops).toEqual(['Foo', 'Bar', 'Baz']);
1291
});
1292
1293
+ it('delays sync updates inside hidden subtrees in Concurrent Mode', () => {
1294
+ const container = document.createElement('div');
1295
+
1296
+ function Baz() {
1297
+ Scheduler.yieldValue('Baz');
1298
+ return <p>baz</p>;
1299
+ }
1300
+
1301
+ let setCounter;
1302
+ function Bar() {
1303
+ const [counter, _setCounter] = React.useState(0);
1304
+ setCounter = _setCounter;
1305
+ Scheduler.yieldValue('Bar');
1306
+ return <p>bar {counter}</p>;
1307
+ }
1308
+
1309
+ function Foo() {
1310
+ Scheduler.yieldValue('Foo');
1311
+ React.useEffect(() => {
1312
+ Scheduler.yieldValue('Foo#effect');
1313
+ });
1314
+ return (
1315
+ <div>
1316
+ <div hidden={true}>
1317
+ <Bar />
1318
+ </div>
1319
+ <Baz />
1320
+ </div>
1321
+ );
1322
+ }
1323
+
1324
+ const root = ReactDOM.unstable_createRoot(container);
1325
+ root.render(<Foo />);
1326
+ if (__DEV__) {
1327
+ expect(Scheduler).toFlushAndYieldThrough([
1328
+ 'Foo',
1329
+ 'Foo',
1330
+ 'Baz',
1331
+ 'Foo#effect',
1332
+ ]);
1333
+ } else {
1334
+ expect(Scheduler).toFlushAndYieldThrough(['Foo', 'Baz', 'Foo#effect']);
1335
+ }
1336
+
1337
+ const hiddenDiv = container.firstChild.firstChild;
1338
+ expect(hiddenDiv.hidden).toBe(true);
1339
+ expect(hiddenDiv.innerHTML).toBe('');
1340
+
1341
+ // Run offscreen update
1342
+ if (__DEV__) {
1343
+ expect(Scheduler).toFlushAndYield(['Bar', 'Bar']);
1344
+ } else {
1345
+ expect(Scheduler).toFlushAndYield(['Bar']);
1346
+ }
1347
+ expect(hiddenDiv.hidden).toBe(true);
1348
+ expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
1349
+
1350
+ ReactDOM.flushSync(() => {
1351
+ setCounter(1);
1352
+ });
1353
+ // Should not flush yet
1354
+ expect(hiddenDiv.innerHTML).toBe('<p>bar 0</p>');
1355
+
1356
+ // Run offscreen update
1357
+ if (__DEV__) {
1358
+ expect(Scheduler).toFlushAndYield(['Bar', 'Bar']);
1359
+ } else {
1360
+ expect(Scheduler).toFlushAndYield(['Bar']);
1361
+ }
1362
+ expect(hiddenDiv.innerHTML).toBe('<p>bar 1</p>');
1363
+ });
1364
+
1365
it('can render ridiculously large number of roots without triggering infinite update loop error', () => {
1366
class Foo extends React.Component {
1367
componentDidMount() {
packages/react-reconciler/src/ReactFiberBeginWork.js
+10
-1
@@ -973,8 +973,8 @@ function updateHostComponent(current, workInProgress, renderExpirationTime) {
973
974
// Check the host config to see if the children are offscreen/hidden.
975
if (
976
- renderExpirationTime !== Never &&
976
workInProgress.mode & ConcurrentMode &&
977
+ renderExpirationTime !== Never &&
978
shouldDeprioritizeSubtree(type, nextProps)
979
) {
980
// Schedule this fiber to re-render at offscreen priority. Then bailout.
@@ -2133,6 +2133,15 @@ function beginWork(
2133
break;
2134
case HostComponent:
2135
pushHostContext(workInProgress);
2136
+ if (
2137
+ workInProgress.mode & ConcurrentMode &&
2138
+ renderExpirationTime !== Never &&
2139
+ shouldDeprioritizeSubtree(workInProgress.type, newProps)
2140
+ ) {
2141
+ // Schedule this fiber to re-render at offscreen priority. Then bailout.
2142
+ workInProgress.expirationTime = workInProgress.childExpirationTime = Never;
2143
+ return null;
2144
+ }
2145
break;
2146
case ClassComponent: {
2147
const Component = workInProgress.type;
packages/react-reconciler/src/__tests__/ReactIncrementalSideEffects-test.internal.js
+1
-1
@@ -966,7 +966,7 @@ describe('ReactIncrementalSideEffects', () => {
966
967
// However, once we render fully, we will have enough time to finish it all
968
// at once.
969
- expect(Scheduler).toFlushAndYield(['Bar', 'Bar', 'Bar']);
969
+ expect(Scheduler).toFlushAndYield(['Bar', 'Bar']);
970
expect(ReactNoop.getChildrenAsJSX()).toEqual(
971
<div>
972
<span prop={1} />