Skip to content

Commit c581cdd

Browse files
authored
Schedule sync updates in microtask (#20872)
* Schedule sync updates in microtask * Updates from review * Fix comment
1 parent 90bde65 commit c581cdd

23 files changed

+121
-51
lines changed

packages/react-dom/src/__tests__/ReactDOMServerPartialHydration-test.internal.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,7 @@ describe('ReactDOMServerPartialHydration', () => {
380380
resolve();
381381
await promise;
382382
Scheduler.unstable_flushAll();
383+
await null;
383384
jest.runAllTimers();
384385

385386
// We should now have hydrated with a ref on the existing span.

packages/react-dom/src/__tests__/ReactServerRenderingHydration-test.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -488,6 +488,7 @@ describe('ReactDOMServerHydration', () => {
488488
jest.runAllTimers();
489489
await Promise.resolve();
490490
Scheduler.unstable_flushAll();
491+
await null;
491492
expect(element.textContent).toBe('Hello world');
492493
});
493494

packages/react-reconciler/src/SchedulerWithReactIntegration.new.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ import {__interactionsRef} from 'scheduler/tracing';
1616
import {
1717
enableSchedulerTracing,
1818
decoupleUpdatePriorityFromScheduler,
19+
enableSyncMicroTasks,
1920
} from 'shared/ReactFeatureFlags';
2021
import invariant from 'shared/invariant';
2122
import {
2223
SyncLanePriority,
2324
getCurrentUpdateLanePriority,
2425
setCurrentUpdateLanePriority,
2526
} from './ReactFiberLane.new';
27+
import {scheduleMicrotask, supportsMicrotasks} from './ReactFiberHostConfig';
2628

2729
const {
2830
unstable_runWithPriority: Scheduler_runWithPriority,
@@ -144,13 +146,19 @@ export function scheduleSyncCallback(callback: SchedulerCallback) {
144146
// the next tick, or earlier if something calls `flushSyncCallbackQueue`.
145147
if (syncQueue === null) {
146148
syncQueue = [callback];
147-
// Flush the queue in the next tick, at the earliest.
149+
148150
// TODO: Figure out how to remove this It's only here as a last resort if we
149151
// 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+
}
154162
} else {
155163
// Push onto existing queue. Don't need to schedule a callback because
156164
// we already scheduled one when we created the queue.

packages/react-reconciler/src/SchedulerWithReactIntegration.old.js

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ import {__interactionsRef} from 'scheduler/tracing';
1616
import {
1717
enableSchedulerTracing,
1818
decoupleUpdatePriorityFromScheduler,
19+
enableSyncMicroTasks,
1920
} from 'shared/ReactFeatureFlags';
2021
import invariant from 'shared/invariant';
2122
import {
2223
SyncLanePriority,
2324
getCurrentUpdateLanePriority,
2425
setCurrentUpdateLanePriority,
2526
} from './ReactFiberLane.old';
27+
import {scheduleMicrotask, supportsMicrotasks} from './ReactFiberHostConfig';
2628

2729
const {
2830
unstable_runWithPriority: Scheduler_runWithPriority,
@@ -144,13 +146,19 @@ export function scheduleSyncCallback(callback: SchedulerCallback) {
144146
// the next tick, or earlier if something calls `flushSyncCallbackQueue`.
145147
if (syncQueue === null) {
146148
syncQueue = [callback];
147-
// Flush the queue in the next tick, at the earliest.
149+
148150
// TODO: Figure out how to remove this It's only here as a last resort if we
149151
// 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+
}
154162
} else {
155163
// Push onto existing queue. Don't need to schedule a callback because
156164
// we already scheduled one when we created the queue.

packages/react-reconciler/src/__tests__/ReactExpiration-test.js

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -598,12 +598,13 @@ describe('ReactExpiration', () => {
598598
// second one.
599599
Scheduler.unstable_advanceTime(1000);
600600
// Attempt to interrupt with a high pri update.
601-
updateHighPri();
601+
await ReactNoop.act(async () => {
602+
updateHighPri();
603+
});
602604

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',
607608
// Then render the high pri update
608609
'High pri: 1',
609610
'Normal pri: 1',

packages/react-reconciler/src/__tests__/ReactHooksWithNoopRenderer-test.js

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1792,7 +1792,7 @@ describe('ReactHooksWithNoopRenderer', () => {
17921792
it(
17931793
'in legacy mode, useEffect is deferred and updates finish synchronously ' +
17941794
'(in a single batch)',
1795-
() => {
1795+
async () => {
17961796
function Counter(props) {
17971797
const [count, updateCount] = useState('(empty)');
17981798
useEffect(() => {
@@ -1807,10 +1807,12 @@ describe('ReactHooksWithNoopRenderer', () => {
18071807
}, [props.count]);
18081808
return <Text text={'Count: ' + count} />;
18091809
}
1810-
act(() => {
1810+
await act(async () => {
18111811
ReactNoop.renderLegacySyncRoot(<Counter count={0} />);
1812+
18121813
// 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)']);
18141816
expect(ReactNoop.getChildren()).toEqual([span('Count: (empty)')]);
18151817
});
18161818

packages/react-reconciler/src/__tests__/ReactIncrementalErrorHandling-test.internal.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1400,6 +1400,10 @@ describe('ReactIncrementalErrorHandling', () => {
14001400
'BrokenRenderAndUnmount componentWillUnmount',
14011401
]);
14021402
expect(ReactNoop.getChildren()).toEqual([]);
1403+
1404+
expect(() => {
1405+
ReactNoop.flushSync();
1406+
}).toThrow('One does not simply unmount me.');
14031407
});
14041408

14051409
it('does not interrupt unmounting if detaching a ref throws', () => {

packages/react-reconciler/src/__tests__/ReactOffscreen-test.js

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,8 +98,11 @@ describe('ReactOffscreen', () => {
9898
<Text text="Outside" />
9999
</>,
100100
);
101+
102+
ReactNoop.flushSync();
103+
101104
// Should not defer the hidden tree
102-
expect(Scheduler).toFlushUntilNextPaint(['A', 'Outside']);
105+
expect(Scheduler).toHaveYielded(['A', 'Outside']);
103106
});
104107
expect(root).toMatchRenderedOutput(
105108
<>

packages/react-reconciler/src/__tests__/ReactSchedulerIntegration-test.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -569,9 +569,11 @@ describe(
569569
ReactNoop.render(<App />);
570570
});
571571

572+
ReactNoop.flushSync();
573+
572574
// Because the render expired, React should finish the tree without
573575
// consulting `shouldYield` again
574-
expect(Scheduler).toFlushExpired(['B', 'C']);
576+
expect(Scheduler).toHaveYielded(['B', 'C']);
575577
});
576578
});
577579
},

packages/react-reconciler/src/__tests__/ReactSuspenseList-test.js

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -292,9 +292,11 @@ describe('ReactSuspenseList', () => {
292292
</>,
293293
);
294294

295-
await C.resolve();
295+
await ReactNoop.act(async () => {
296+
C.resolve();
297+
});
296298

297-
expect(Scheduler).toFlushAndYield(['C']);
299+
expect(Scheduler).toHaveYielded(['C']);
298300

299301
expect(ReactNoop).toMatchRenderedOutput(
300302
<>
@@ -304,9 +306,11 @@ describe('ReactSuspenseList', () => {
304306
</>,
305307
);
306308

307-
await B.resolve();
309+
await ReactNoop.act(async () => {
310+
B.resolve();
311+
});
308312

309-
expect(Scheduler).toFlushAndYield(['B']);
313+
expect(Scheduler).toHaveYielded(['B']);
310314

311315
expect(ReactNoop).toMatchRenderedOutput(
312316
<>

0 commit comments

Comments
 (0)