Skip to content

Commit ef37d55

Browse files
authored
Use performConcurrentWorkOnRoot for "sync default" (#21322)
Instead of `performSyncWorkOnRoot`. The conceptual model is that the only difference between sync default updates (in React 18) and concurrent default updates (in a future major release) is time slicing. All other behavior should be the same (i.e. the stuff in `finishConcurrentRender`). Given this, I think it makes more sense to model the implementation this way, too. This exposed a quirk in the previous implementation where non-sync work was sometimes mistaken for sync work and flushed too early. In the new implementation, `performSyncWorkOnRoot` is only used for truly synchronous renders (i.e. `SyncLane`), which should make these mistakes less common. Fixes most of the tests marked with TODOs from #21072.
1 parent a632f7d commit ef37d55

9 files changed

+45
-99
lines changed

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
263263
// Default priority updates should not interrupt transition updates. The
264264
// only difference between default updates and transition updates is that
265265
// default updates do not support refresh transitions.
266+
// TODO: This applies to sync default updates, too. Which is probably what
267+
// we want for default priority events, but not for continuous priority
268+
// events like hover.
266269
(nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes)
267270
) {
268271
// Keep working on the existing in-progress tree. Do not interrupt.

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -263,6 +263,9 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
263263
// Default priority updates should not interrupt transition updates. The
264264
// only difference between default updates and transition updates is that
265265
// default updates do not support refresh transitions.
266+
// TODO: This applies to sync default updates, too. Which is probably what
267+
// we want for default priority events, but not for continuous priority
268+
// events like hover.
266269
(nextLane === DefaultLane && (wipLane & TransitionLanes) !== NoLanes)
267270
) {
268271
// Keep working on the existing in-progress tree. Do not interrupt.

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

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -713,16 +713,7 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
713713

714714
// Schedule a new callback.
715715
let newCallbackNode;
716-
if (
717-
enableSyncDefaultUpdates &&
718-
(newCallbackPriority === DefaultLane ||
719-
newCallbackPriority === DefaultHydrationLane)
720-
) {
721-
newCallbackNode = scheduleCallback(
722-
ImmediateSchedulerPriority,
723-
performSyncWorkOnRoot.bind(null, root),
724-
);
725-
} else if (newCallbackPriority === SyncLane) {
716+
if (newCallbackPriority === SyncLane) {
726717
// Special case: Sync React callbacks are scheduled on a special
727718
// internal queue
728719
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
@@ -820,7 +811,13 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
820811
return null;
821812
}
822813

823-
let exitStatus = renderRootConcurrent(root, lanes);
814+
let exitStatus =
815+
enableSyncDefaultUpdates &&
816+
(includesSomeLane(lanes, DefaultLane) ||
817+
includesSomeLane(lanes, DefaultHydrationLane))
818+
? // Time slicing is disabled for default updates in this root.
819+
renderRootSync(root, lanes)
820+
: renderRootConcurrent(root, lanes);
824821
if (exitStatus !== RootIncomplete) {
825822
if (exitStatus === RootErrored) {
826823
executionContext |= RetryAfterError;
@@ -1017,13 +1014,7 @@ function performSyncWorkOnRoot(root) {
10171014
// rendering it before rendering the rest of the expired work.
10181015
lanes = workInProgressRootRenderLanes;
10191016
}
1020-
} else if (
1021-
!(
1022-
enableSyncDefaultUpdates &&
1023-
(includesSomeLane(lanes, DefaultLane) ||
1024-
includesSomeLane(lanes, DefaultHydrationLane))
1025-
)
1026-
) {
1017+
} else {
10271018
// There's no remaining sync work left.
10281019
ensureRootIsScheduled(root, now());
10291020
return null;
@@ -1067,15 +1058,7 @@ function performSyncWorkOnRoot(root) {
10671058
const finishedWork: Fiber = (root.current.alternate: any);
10681059
root.finishedWork = finishedWork;
10691060
root.finishedLanes = lanes;
1070-
if (
1071-
enableSyncDefaultUpdates &&
1072-
(includesSomeLane(lanes, DefaultLane) ||
1073-
includesSomeLane(lanes, DefaultHydrationLane))
1074-
) {
1075-
finishConcurrentRender(root, exitStatus, lanes);
1076-
} else {
1077-
commitRoot(root);
1078-
}
1061+
commitRoot(root);
10791062

10801063
// Before exiting, make sure there's a callback scheduled for the next
10811064
// pending level.

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

Lines changed: 10 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -713,16 +713,7 @@ function ensureRootIsScheduled(root: FiberRoot, currentTime: number) {
713713

714714
// Schedule a new callback.
715715
let newCallbackNode;
716-
if (
717-
enableSyncDefaultUpdates &&
718-
(newCallbackPriority === DefaultLane ||
719-
newCallbackPriority === DefaultHydrationLane)
720-
) {
721-
newCallbackNode = scheduleCallback(
722-
ImmediateSchedulerPriority,
723-
performSyncWorkOnRoot.bind(null, root),
724-
);
725-
} else if (newCallbackPriority === SyncLane) {
716+
if (newCallbackPriority === SyncLane) {
726717
// Special case: Sync React callbacks are scheduled on a special
727718
// internal queue
728719
scheduleSyncCallback(performSyncWorkOnRoot.bind(null, root));
@@ -820,7 +811,13 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
820811
return null;
821812
}
822813

823-
let exitStatus = renderRootConcurrent(root, lanes);
814+
let exitStatus =
815+
enableSyncDefaultUpdates &&
816+
(includesSomeLane(lanes, DefaultLane) ||
817+
includesSomeLane(lanes, DefaultHydrationLane))
818+
? // Time slicing is disabled for default updates in this root.
819+
renderRootSync(root, lanes)
820+
: renderRootConcurrent(root, lanes);
824821
if (exitStatus !== RootIncomplete) {
825822
if (exitStatus === RootErrored) {
826823
executionContext |= RetryAfterError;
@@ -1017,13 +1014,7 @@ function performSyncWorkOnRoot(root) {
10171014
// rendering it before rendering the rest of the expired work.
10181015
lanes = workInProgressRootRenderLanes;
10191016
}
1020-
} else if (
1021-
!(
1022-
enableSyncDefaultUpdates &&
1023-
(includesSomeLane(lanes, DefaultLane) ||
1024-
includesSomeLane(lanes, DefaultHydrationLane))
1025-
)
1026-
) {
1017+
} else {
10271018
// There's no remaining sync work left.
10281019
ensureRootIsScheduled(root, now());
10291020
return null;
@@ -1067,15 +1058,7 @@ function performSyncWorkOnRoot(root) {
10671058
const finishedWork: Fiber = (root.current.alternate: any);
10681059
root.finishedWork = finishedWork;
10691060
root.finishedLanes = lanes;
1070-
if (
1071-
enableSyncDefaultUpdates &&
1072-
(includesSomeLane(lanes, DefaultLane) ||
1073-
includesSomeLane(lanes, DefaultHydrationLane))
1074-
) {
1075-
finishConcurrentRender(root, exitStatus, lanes);
1076-
} else {
1077-
commitRoot(root);
1078-
}
1061+
commitRoot(root);
10791062

10801063
// Before exiting, make sure there's a callback scheduled for the next
10811064
// pending level.

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

Lines changed: 1 addition & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -440,12 +440,7 @@ describe('ReactExpiration', () => {
440440
flushNextRenderIfExpired();
441441
expect(Scheduler).toHaveYielded([]);
442442

443-
if (gate(flags => flags.enableSyncDefaultUpdates)) {
444-
// TODO: Why is this flushed?
445-
expect(ReactNoop).toMatchRenderedOutput('Hi');
446-
} else {
447-
expect(ReactNoop).toMatchRenderedOutput(null);
448-
}
443+
expect(ReactNoop).toMatchRenderedOutput(null);
449444

450445
// Advance the time some more to expire the update.
451446
Scheduler.unstable_advanceTime(10000);
@@ -477,8 +472,6 @@ describe('ReactExpiration', () => {
477472
// Advancing by ~5 seconds should be sufficient to expire the update. (I
478473
// used a slightly larger number to allow for possible rounding.)
479474
Scheduler.unstable_advanceTime(6000);
480-
481-
ReactNoop.render('Hi');
482475
flushNextRenderIfExpired();
483476
expect(Scheduler).toHaveYielded([]);
484477
expect(ReactNoop).toMatchRenderedOutput('Hi');

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

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -51,24 +51,15 @@ describe('ReactFlushSync', () => {
5151
// The passive effect will schedule a sync update and a normal update.
5252
// They should commit in two separate batches. First the sync one.
5353
expect(() => {
54-
if (gate(flags => flags.enableSyncDefaultUpdates)) {
55-
expect(Scheduler).toFlushUntilNextPaint(['1, 0', '1, 1']);
56-
} else {
57-
expect(Scheduler).toFlushUntilNextPaint(['1, 0']);
58-
}
54+
expect(Scheduler).toFlushUntilNextPaint(['1, 0']);
5955
}).toErrorDev('flushSync was called from inside a lifecycle method');
6056

6157
// The remaining update is not sync
6258
ReactNoop.flushSync();
6359
expect(Scheduler).toHaveYielded([]);
6460

6561
// Now flush it.
66-
if (gate(flags => flags.enableSyncDefaultUpdates)) {
67-
// With sync default updates, passive effects are synchronously flushed.
68-
expect(Scheduler).toHaveYielded([]);
69-
} else {
70-
expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
71-
}
62+
expect(Scheduler).toFlushUntilNextPaint(['1, 1']);
7263
});
7364
expect(root).toMatchRenderedOutput('1, 1');
7465
});

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

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1406,20 +1406,21 @@ describe('ReactHooksWithNoopRenderer', () => {
14061406
setParentState(false);
14071407
});
14081408
if (gate(flags => flags.enableSyncDefaultUpdates)) {
1409+
// TODO: Default updates do not interrupt transition updates, to
1410+
// prevent starvation. However, when sync default updates are enabled,
1411+
// continuous updates are treated like default updates. In this case,
1412+
// we probably don't want this behavior; continuous should be allowed
1413+
// to interrupt.
14091414
expect(Scheduler).toFlushUntilNextPaint([
1410-
// TODO: why do the children render and fire effects?
14111415
'Child two render',
14121416
'Child one commit',
14131417
'Child two commit',
1414-
'Parent false render',
1415-
'Parent false commit',
1416-
]);
1417-
} else {
1418-
expect(Scheduler).toFlushUntilNextPaint([
1419-
'Parent false render',
1420-
'Parent false commit',
14211418
]);
14221419
}
1420+
expect(Scheduler).toFlushUntilNextPaint([
1421+
'Parent false render',
1422+
'Parent false commit',
1423+
]);
14231424

14241425
// Schedule updates for children too (which should be ignored)
14251426
setChildStates.forEach(setChildState => setChildState(2));

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

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,9 @@ describe('ReactIncrementalUpdates', () => {
6262
ReactNoop.render(<Foo />);
6363
expect(Scheduler).toFlushAndYieldThrough(['commit']);
6464

65-
if (gate(flags => flags.enableSyncDefaultUpdates)) {
66-
// TODO: should deferredUpdates flush sync with the default update?
67-
expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
68-
expect(Scheduler).toFlushWithoutYielding();
69-
} else {
70-
expect(state).toEqual({a: 'a'});
71-
expect(Scheduler).toFlushWithoutYielding();
72-
expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
73-
}
65+
expect(state).toEqual({a: 'a'});
66+
expect(Scheduler).toFlushWithoutYielding();
67+
expect(state).toEqual({a: 'a', b: 'b', c: 'c'});
7468
});
7569

7670
it('applies updates with equal priority in insertion order', () => {

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

Lines changed: 4 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1569,15 +1569,10 @@ describe('useMutableSource', () => {
15691569
mutateB('b0');
15701570
});
15711571
// Finish the current render
1572-
if (gate(flags => flags.enableSyncDefaultUpdates)) {
1573-
// Default sync will flush both without yielding
1574-
expect(Scheduler).toFlushUntilNextPaint(['c', 'a0']);
1575-
} else {
1576-
expect(Scheduler).toFlushUntilNextPaint(['c']);
1577-
// a0 will re-render because of the mutation update. But it should show
1578-
// the latest value, not the intermediate one, to avoid tearing with b.
1579-
expect(Scheduler).toFlushUntilNextPaint(['a0']);
1580-
}
1572+
expect(Scheduler).toFlushUntilNextPaint(['c']);
1573+
// a0 will re-render because of the mutation update. But it should show
1574+
// the latest value, not the intermediate one, to avoid tearing with b.
1575+
expect(Scheduler).toFlushUntilNextPaint(['a0']);
15811576

15821577
expect(root).toMatchRenderedOutput('a0b0c');
15831578
// We should be done.

0 commit comments

Comments
 (0)