Schedule sync updates in microtask (#20872)
* Schedule sync updates in microtask * Updates from review * Fix comment
Ricky committed
Feb 25, 2021 at 17:25 UTC
c581cdd480f71af6ee87168c72ecabb87b6984c3
23 files changed
+121
-51
packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js
+1
@@ -380,6 +380,7 @@ describe('ReactDOMServerPartialHydration', () => {
380
resolve();
381
await promise;
382
Scheduler.unstable_flushAll();
383
+ await null;
384
jest.runAllTimers();
385
386
// We should now have hydrated with a ref on the existing span.
packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js
+1
@@ -488,6 +488,7 @@ describe('ReactDOMServerHydration', () => {
488
jest.runAllTimers();
489
await Promise.resolve();
490
Scheduler.unstable_flushAll();
491
+ await null;
492
expect(element.textContent).toBe('Hello world');
493
});
494
packages/react-reconciler/src/SchedulerWithReactIntegration.new.js
+13
-5
@@ -16,6 +16,7 @@ import {__interactionsRef} from 'scheduler/tracing';
16
import {
17
enableSchedulerTracing,
18
decoupleUpdatePriorityFromScheduler,
19
+ enableSyncMicroTasks,
20
} from 'shared/ReactFeatureFlags';
21
import invariant from 'shared/invariant';
22
import {
@@ -23,6 +24,7 @@ import {
24
getCurrentUpdateLanePriority,
25
setCurrentUpdateLanePriority,
26
} from './ReactFiberLane.new';
27
+import {scheduleMicrotask, supportsMicrotasks} from './ReactFiberHostConfig';
28
29
const {
30
unstable_runWithPriority: Scheduler_runWithPriority,
@@ -144,13 +146,19 @@ export function scheduleSyncCallback(callback: SchedulerCallback) {
146
// the next tick, or earlier if something calls `flushSyncCallbackQueue`.
147
if (syncQueue === null) {
148
syncQueue = [callback];
147
- // Flush the queue in the next tick, at the earliest.
149
+
150
// TODO: Figure out how to remove this It's only here as a last resort if we
151
// forget to explicitly flush.
150
- immediateQueueCallbackNode = Scheduler_scheduleCallback(
151
- Scheduler_ImmediatePriority,
152
- flushSyncCallbackQueueImpl,
153
- );
152
+ if (enableSyncMicroTasks && supportsMicrotasks) {
153
+ // Flush the queue in a microtask.
154
+ scheduleMicrotask(flushSyncCallbackQueueImpl);
155
+ } else {
156
+ // Flush the queue in the next tick.
157
+ immediateQueueCallbackNode = Scheduler_scheduleCallback(
158
+ Scheduler_ImmediatePriority,
159
+ flushSyncCallbackQueueImpl,
160
+ );
161
+ }
162
} else {
163
// Push onto existing queue. Don't need to schedule a callback because
164
// we already scheduled one when we created the queue.
packages/react-reconciler/src/SchedulerWithReactIntegration.old.js
+13
-5
@@ -16,6 +16,7 @@ import {__interactionsRef} from 'scheduler/tracing';
16
import {
17
enableSchedulerTracing,
18
decoupleUpdatePriorityFromScheduler,
19
+ enableSyncMicroTasks,
20
} from 'shared/ReactFeatureFlags';
21
import invariant from 'shared/invariant';
22
import {
@@ -23,6 +24,7 @@ import {
24
getCurrentUpdateLanePriority,
25
setCurrentUpdateLanePriority,
26
} from './ReactFiberLane.old';
27
+import {scheduleMicrotask, supportsMicrotasks} from './ReactFiberHostConfig';
28
29
const {
30
unstable_runWithPriority: Scheduler_runWithPriority,
@@ -144,13 +146,19 @@ export function scheduleSyncCallback(callback: SchedulerCallback) {
146
// the next tick, or earlier if something calls `flushSyncCallbackQueue`.
147
if (syncQueue === null) {
148
syncQueue = [callback];
147
- // Flush the queue in the next tick, at the earliest.
149
+
150
// TODO: Figure out how to remove this It's only here as a last resort if we
151
// forget to explicitly flush.
150
- immediateQueueCallbackNode = Scheduler_scheduleCallback(
151
- Scheduler_ImmediatePriority,
152
- flushSyncCallbackQueueImpl,
153
- );
152
+ if (enableSyncMicroTasks && supportsMicrotasks) {
153
+ // Flush the queue in a microtask.
154
+ scheduleMicrotask(flushSyncCallbackQueueImpl);
155
+ } else {
156
+ // Flush the queue in the next tick.
157
+ immediateQueueCallbackNode = Scheduler_scheduleCallback(
158
+ Scheduler_ImmediatePriority,
159
+ flushSyncCallbackQueueImpl,
160
+ );
161
+ }
162
} else {
163
// Push onto existing queue. Don't need to schedule a callback because
164
// we already scheduled one when we created the queue.
packages/react-reconciler/src/__tests__/ReactExpiration-test.js
+6
-5
@@ -598,12 +598,13 @@ describe('ReactExpiration', () => {
598
// second one.
599
Scheduler.unstable_advanceTime(1000);
600
// Attempt to interrupt with a high pri update.
601
- updateHighPri();
601
+ await ReactNoop.act(async () => {
602
+ updateHighPri();
603
+ });
604
603
- // The first update expired, so first will finish it without
604
- // interrupting. But not the second update, which hasn't expired yet.
605
- expect(Scheduler).toFlushExpired(['Sibling']);
606
- expect(Scheduler).toFlushAndYield([
605
+ expect(Scheduler).toHaveYielded([
606
+ // The first update expired
607
+ 'Sibling',
608
// Then render the high pri update
609
'High pri: 1',
610
'Normal pri: 1',
packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js
+5
-3
@@ -1792,7 +1792,7 @@ describe('ReactHooksWithNoopRenderer', () => {
1792
it(
1793
'in legacy mode, useEffect is deferred and updates finish synchronously ' +
1794
'(in a single batch)',
1795
- () => {
1795
+ async () => {
1796
function Counter(props) {
1797
const [count, updateCount] = useState('(empty)');
1798
useEffect(() => {
@@ -1807,10 +1807,12 @@ describe('ReactHooksWithNoopRenderer', () => {
1807
}, [props.count]);
1808
return <Text text={'Count: ' + count} />;
1809
}
1810
- act(() => {
1810
+ await act(async () => {
1811
ReactNoop.renderLegacySyncRoot(<Counter count={0} />);
1812
+
1813
// Even in legacy mode, effects are deferred until after paint
1813
- expect(Scheduler).toFlushAndYieldThrough(['Count: (empty)']);
1814
+ ReactNoop.flushSync();
1815
+ expect(Scheduler).toHaveYielded(['Count: (empty)']);
1816
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
1817
});
1818
packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js
+4
@@ -1400,6 +1400,10 @@ describe('ReactIncrementalErrorHandling', () => {
1400
'BrokenRenderAndUnmount componentWillUnmount',
1401
]);
1402
expect(ReactNoop.getChildren()).toEqual([]);
1403
+
1404
+ expect(() => {
1405
+ ReactNoop.flushSync();
1406
+ }).toThrow('One does not simply unmount me.');
1407
});
1408
1409
it('does not interrupt unmounting if detaching a ref throws', () => {
packages/react-reconciler/src/__tests__/ReactOffscreen-test.js
+4
-1
@@ -98,8 +98,11 @@ describe('ReactOffscreen', () => {
98
<Text text="Outside" />
99
</>,
100
);
101
+
102
+ ReactNoop.flushSync();
103
+
104
// Should not defer the hidden tree
102
- expect(Scheduler).toFlushUntilNextPaint(['A', 'Outside']);
105
+ expect(Scheduler).toHaveYielded(['A', 'Outside']);
106
});
107
expect(root).toMatchRenderedOutput(
108
<>
packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js
+3
-1
@@ -569,9 +569,11 @@ describe(
569
ReactNoop.render(<App />);
570
});
571
572
+ ReactNoop.flushSync();
573
+
574
// Because the render expired, React should finish the tree without
575
// consulting `shouldYield` again
574
- expect(Scheduler).toFlushExpired(['B', 'C']);
576
+ expect(Scheduler).toHaveYielded(['B', 'C']);
577
});
578
});
579
},
packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js
+8
-4
@@ -292,9 +292,11 @@ describe('ReactSuspenseList', () => {
292
</>,
293
);
294
295
- await C.resolve();
295
+ await ReactNoop.act(async () => {
296
+ C.resolve();
297
+ });
298
297
- expect(Scheduler).toFlushAndYield(['C']);
299
+ expect(Scheduler).toHaveYielded(['C']);
300
301
expect(ReactNoop).toMatchRenderedOutput(
302
<>
@@ -304,9 +306,11 @@ describe('ReactSuspenseList', () => {
306
</>,
307
);
308
307
- await B.resolve();
309
+ await ReactNoop.act(async () => {
310
+ B.resolve();
311
+ });
312
309
- expect(Scheduler).toFlushAndYield(['B']);
313
+ expect(Scheduler).toHaveYielded(['B']);
314
315
expect(ReactNoop).toMatchRenderedOutput(
316
<>
packages/react-reconciler/src/__tests__/ReactSuspensePlaceholder-test.internal.js
+10
-4
@@ -310,7 +310,7 @@ describe('ReactSuspensePlaceholder', () => {
310
});
311
312
describe('when suspending during mount', () => {
313
- it('properly accounts for base durations when a suspended times out in a legacy tree', () => {
313
+ it('properly accounts for base durations when a suspended times out in a legacy tree', async () => {
314
ReactNoop.renderLegacySyncRoot(<App shouldSuspend={true} />);
315
expect(Scheduler).toHaveYielded([
316
'App',
@@ -331,7 +331,10 @@ describe('ReactSuspensePlaceholder', () => {
331
jest.advanceTimersByTime(1000);
332
333
expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
334
- expect(Scheduler).toFlushExpired(['Loaded']);
334
+
335
+ ReactNoop.flushSync();
336
+
337
+ expect(Scheduler).toHaveYielded(['Loaded']);
338
expect(ReactNoop).toMatchRenderedOutput('LoadedText');
339
expect(onRender).toHaveBeenCalledTimes(2);
340
@@ -378,7 +381,7 @@ describe('ReactSuspensePlaceholder', () => {
381
});
382
383
describe('when suspending during update', () => {
381
- it('properly accounts for base durations when a suspended times out in a legacy tree', () => {
384
+ it('properly accounts for base durations when a suspended times out in a legacy tree', async () => {
385
ReactNoop.renderLegacySyncRoot(
386
<App shouldSuspend={false} textRenderDuration={5} />,
387
);
@@ -427,7 +430,10 @@ describe('ReactSuspensePlaceholder', () => {
430
jest.advanceTimersByTime(1000);
431
432
expect(Scheduler).toHaveYielded(['Promise resolved [Loaded]']);
430
- expect(Scheduler).toFlushExpired(['Loaded']);
433
+
434
+ ReactNoop.flushSync();
435
+
436
+ expect(Scheduler).toHaveYielded(['Loaded']);
437
expect(ReactNoop).toMatchRenderedOutput('LoadedNew');
438
expect(onRender).toHaveBeenCalledTimes(4);
439
packages/react-reconciler/src/__tests__/ReactSuspenseWithNoopRenderer-test.js
+36
-18
@@ -1088,9 +1088,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1088
expect(Scheduler).toHaveYielded(['Suspend! [Result]', 'Loading...']);
1089
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1090
1091
- await resolveText('Result');
1091
+ await ReactNoop.act(async () => {
1092
+ resolveText('Result');
1093
+ });
1094
1093
- expect(Scheduler).toFlushExpired(['Result']);
1095
+ expect(Scheduler).toHaveYielded(['Result']);
1096
expect(ReactNoop.getChildren()).toEqual([span('Result')]);
1097
});
1098
@@ -1156,8 +1158,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1158
</>,
1159
);
1160
1159
- await resolveText('Step: 2');
1160
- expect(Scheduler).toFlushExpired(['Step: 2']);
1161
+ await ReactNoop.act(async () => {
1162
+ resolveText('Step: 2');
1163
+ });
1164
+ expect(Scheduler).toHaveYielded(['Step: 2']);
1165
expect(ReactNoop).toMatchRenderedOutput(
1166
<>
1167
<span prop="Step: 2" />
@@ -1227,9 +1231,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1231
</>,
1232
);
1233
1230
- await resolveText('B');
1234
+ await ReactNoop.act(async () => {
1235
+ resolveText('B');
1236
+ });
1237
1232
- expect(Scheduler).toFlushExpired(['B']);
1238
+ expect(Scheduler).toHaveYielded(['B']);
1239
expect(ReactNoop).toMatchRenderedOutput(
1240
<>
1241
<span prop="A" />
@@ -1271,9 +1277,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1277
]);
1278
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1279
1274
- await resolveText('Hi');
1280
+ await ReactNoop.act(async () => {
1281
+ resolveText('Hi');
1282
+ });
1283
1276
- expect(Scheduler).toFlushExpired([
1284
+ expect(Scheduler).toHaveYielded([
1285
'constructor',
1286
'Hi',
1287
'componentDidMount',
@@ -1316,8 +1324,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1324
'Loading...',
1325
]);
1326
expect(ReactNoop.getChildren()).toEqual([span('Loading...')]);
1319
- await resolveText('Hi');
1320
- expect(Scheduler).toFlushExpired(['Hi']);
1327
+ await ReactNoop.act(async () => {
1328
+ resolveText('Hi');
1329
+ });
1330
+ expect(Scheduler).toHaveYielded(['Hi']);
1331
expect(ReactNoop.getChildren()).toEqual([span('Hi')]);
1332
});
1333
@@ -1360,8 +1370,10 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1370
</>,
1371
]);
1372
1363
- await resolveText('Hi');
1364
- expect(Scheduler).toFlushExpired(['Hi']);
1373
+ await ReactNoop.act(async () => {
1374
+ resolveText('Hi');
1375
+ });
1376
+ expect(Scheduler).toHaveYielded(['Hi']);
1377
});
1378
} else {
1379
// @gate enableCache
@@ -1401,9 +1413,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1413
'Child is hidden: true',
1414
]);
1415
1404
- await resolveText('Hi');
1416
+ await ReactNoop.act(async () => {
1417
+ resolveText('Hi');
1418
+ });
1419
1406
- expect(Scheduler).toFlushExpired(['Hi']);
1420
+ expect(Scheduler).toHaveYielded(['Hi']);
1421
});
1422
}
1423
@@ -1647,9 +1661,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1661
</>,
1662
);
1663
1650
- await resolveText('B');
1664
+ await ReactNoop.act(async () => {
1665
+ resolveText('B');
1666
+ });
1667
1652
- expect(Scheduler).toFlushAndYield([
1668
+ expect(Scheduler).toHaveYielded([
1669
'B',
1670
'Destroy Layout Effect [Loading...]',
1671
'Layout Effect [B]',
@@ -1681,9 +1697,11 @@ describe('ReactSuspenseWithNoopRenderer', () => {
1697
'Effect [Loading...]',
1698
]);
1699
1684
- await resolveText('B2');
1700
+ await ReactNoop.act(async () => {
1701
+ resolveText('B2');
1702
+ });
1703
1686
- expect(Scheduler).toFlushAndYield([
1704
+ expect(Scheduler).toHaveYielded([
1705
'B2',
1706
'Destroy Layout Effect [Loading...]',
1707
'Destroy Layout Effect [B]',
packages/react-reconciler/src/__tests__/useMutableSource-test.internal.js
+6
-5
@@ -1794,7 +1794,7 @@ describe('useMutableSource', () => {
1794
});
1795
1796
// @gate experimental
1797
- it('should not misidentify mutations after render as side effects', () => {
1797
+ it('should not misidentify mutations after render as side effects', async () => {
1798
const source = createSource('initial');
1799
const mutableSource = createMutableSource(
1800
source,
@@ -1811,15 +1811,16 @@ describe('useMutableSource', () => {
1811
return null;
1812
}
1813
1814
- act(() => {
1814
+ await act(async () => {
1815
ReactNoop.renderLegacySyncRoot(
1816
<React.StrictMode>
1817
<MutateDuringRead />
1818
</React.StrictMode>,
1819
);
1820
- expect(Scheduler).toFlushAndYieldThrough([
1821
- 'MutateDuringRead:initial',
1822
- ]);
1820
+ });
1821
+ expect(Scheduler).toHaveYielded(['MutateDuringRead:initial']);
1822
+
1823
+ await act(async () => {
1824
source.value = 'updated';
1825
});
1826
expect(Scheduler).toHaveYielded(['MutateDuringRead:updated']);
packages/shared/ReactFeatureFlags.js
+2
@@ -155,4 +155,6 @@ export const disableSchedulerTimeoutInWorkLoop = false;
155
156
export const enableDiscreteEventMicroTasks = false;
157
158
+export const enableSyncMicroTasks = false;
159
+
160
export const enableNativeEventPriorityInference = false;
packages/shared/forks/ReactFeatureFlags.native-fb.js
+1
@@ -59,6 +59,7 @@ export const enableUseRefAccessWarning = false;
59
export const enableRecursiveCommitTraversal = false;
60
export const disableSchedulerTimeoutInWorkLoop = false;
61
export const enableDiscreteEventMicroTasks = false;
62
+export const enableSyncMicroTasks = false;
63
export const enableNativeEventPriorityInference = false;
64
65
// Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.native-oss.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58
export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableDiscreteEventMicroTasks = false;
61
+export const enableSyncMicroTasks = false;
62
export const enableNativeEventPriorityInference = false;
63
64
// Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.test-renderer.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58
export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableDiscreteEventMicroTasks = false;
61
+export const enableSyncMicroTasks = false;
62
export const enableNativeEventPriorityInference = false;
63
64
// Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.test-renderer.native.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58
export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableDiscreteEventMicroTasks = false;
61
+export const enableSyncMicroTasks = false;
62
export const enableNativeEventPriorityInference = false;
63
64
// Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.test-renderer.www.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58
export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableDiscreteEventMicroTasks = false;
61
+export const enableSyncMicroTasks = false;
62
export const enableNativeEventPriorityInference = false;
63
64
// Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.testing.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58
export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableDiscreteEventMicroTasks = false;
61
+export const enableSyncMicroTasks = false;
62
export const enableNativeEventPriorityInference = false;
63
64
// Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.testing.www.js
+1
@@ -58,6 +58,7 @@ export const enableUseRefAccessWarning = false;
58
export const enableRecursiveCommitTraversal = false;
59
export const disableSchedulerTimeoutInWorkLoop = false;
60
export const enableDiscreteEventMicroTasks = false;
61
+export const enableSyncMicroTasks = false;
62
export const enableNativeEventPriorityInference = false;
63
64
// Flow magic to verify the exports of this file match the original version.
packages/shared/forks/ReactFeatureFlags.www-dynamic.js
+1
@@ -57,4 +57,5 @@ export const enableUseRefAccessWarning = __VARIANT__;
57
export const enableProfilerNestedUpdateScheduledHook = __VARIANT__;
58
export const disableSchedulerTimeoutInWorkLoop = __VARIANT__;
59
export const enableDiscreteEventMicroTasks = __VARIANT__;
60
+export const enableSyncMicroTasks = __VARIANT__;
61
export const enableNativeEventPriorityInference = __VARIANT__;
packages/shared/forks/ReactFeatureFlags.www.js
+1
@@ -33,6 +33,7 @@ export const {
33
disableNativeComponentFrames,
34
disableSchedulerTimeoutInWorkLoop,
35
enableDiscreteEventMicroTasks,
36
+ enableSyncMicroTasks,
37
enableNativeEventPriorityInference,
38
} = dynamicFeatureFlags;
39