Skip to content

Commit 3477cae

Browse files
committed
Continuous updates should interrupt transitions
Even when updates are sync by default. Discovered this quirk while working on facebook#21322. Previously, when sync default updates are enabled, continuous updates are treated like default updates. We implemented this by assigning DefaultLane to continous updates. However, an unintended consequence of that approach is that continuous updates would no longer interrupt transitions, because default updates are not supposed to interrupt transitions. To fix this, I changed the implementation to always assign separate lanes for default and continuous updates. Then I entangle the lanes together.
1 parent 1210c37 commit 3477cae

7 files changed

+93
-57
lines changed

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
enableCache,
4040
enableSchedulingProfiler,
4141
enableUpdaterTracking,
42+
enableSyncDefaultUpdates,
4243
} from 'shared/ReactFeatureFlags';
4344
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';
4445

@@ -273,6 +274,18 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
273274
}
274275
}
275276

277+
if (
278+
// TODO: Check for root override, once that lands
279+
enableSyncDefaultUpdates &&
280+
(nextLanes & InputContinuousLane) !== NoLanes
281+
) {
282+
// When updates are sync by default, we entangle continous priority updates
283+
// and default updates, so they render in the same batch. The only reason
284+
// they use separate lanes is because continuous updates should interrupt
285+
// transitions, but default updates should not.
286+
nextLanes |= pendingLanes & DefaultLane;
287+
}
288+
276289
// Check for entangled lanes and add them to the batch.
277290
//
278291
// A lane is said to be entangled with another when it's not allowed to render

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,7 @@ import {
3939
enableCache,
4040
enableSchedulingProfiler,
4141
enableUpdaterTracking,
42+
enableSyncDefaultUpdates,
4243
} from 'shared/ReactFeatureFlags';
4344
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';
4445

@@ -273,6 +274,18 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
273274
}
274275
}
275276

277+
if (
278+
// TODO: Check for root override, once that lands
279+
enableSyncDefaultUpdates &&
280+
(nextLanes & InputContinuousLane) !== NoLanes
281+
) {
282+
// When updates are sync by default, we entangle continous priority updates
283+
// and default updates, so they render in the same batch. The only reason
284+
// they use separate lanes is because continuous updates should interrupt
285+
// transitions, but default updates should not.
286+
nextLanes |= pendingLanes & DefaultLane;
287+
}
288+
276289
// Check for entangled lanes and add them to the batch.
277290
//
278291
// A lane is said to be entangled with another when it's not allowed to render

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

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ import {
139139
NoLanes,
140140
NoLane,
141141
SyncLane,
142-
DefaultLane,
143142
DefaultHydrationLane,
143+
DefaultLane,
144144
InputContinuousLane,
145145
InputContinuousHydrationLane,
146146
NoTimestamp,
@@ -437,13 +437,6 @@ export function requestUpdateLane(fiber: Fiber): Lane {
437437
// TODO: Move this type conversion to the event priority module.
438438
const updateLane: Lane = (getCurrentUpdatePriority(): any);
439439
if (updateLane !== NoLane) {
440-
if (
441-
enableSyncDefaultUpdates &&
442-
(updateLane === InputContinuousLane ||
443-
updateLane === InputContinuousHydrationLane)
444-
) {
445-
return DefaultLane;
446-
}
447440
return updateLane;
448441
}
449442

@@ -454,13 +447,6 @@ export function requestUpdateLane(fiber: Fiber): Lane {
454447
// use that directly.
455448
// TODO: Move this type conversion to the event priority module.
456449
const eventLane: Lane = (getCurrentEventPriority(): any);
457-
if (
458-
enableSyncDefaultUpdates &&
459-
(eventLane === InputContinuousLane ||
460-
eventLane === InputContinuousHydrationLane)
461-
) {
462-
return DefaultLane;
463-
}
464450
return eventLane;
465451
}
466452

@@ -814,7 +800,9 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
814800
let exitStatus =
815801
enableSyncDefaultUpdates &&
816802
(includesSomeLane(lanes, DefaultLane) ||
817-
includesSomeLane(lanes, DefaultHydrationLane))
803+
includesSomeLane(lanes, InputContinuousLane) ||
804+
includesSomeLane(lanes, DefaultHydrationLane) ||
805+
includesSomeLane(lanes, InputContinuousHydrationLane))
818806
? // Time slicing is disabled for default updates in this root.
819807
renderRootSync(root, lanes)
820808
: renderRootConcurrent(root, lanes);

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

Lines changed: 4 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -139,8 +139,8 @@ import {
139139
NoLanes,
140140
NoLane,
141141
SyncLane,
142-
DefaultLane,
143142
DefaultHydrationLane,
143+
DefaultLane,
144144
InputContinuousLane,
145145
InputContinuousHydrationLane,
146146
NoTimestamp,
@@ -437,13 +437,6 @@ export function requestUpdateLane(fiber: Fiber): Lane {
437437
// TODO: Move this type conversion to the event priority module.
438438
const updateLane: Lane = (getCurrentUpdatePriority(): any);
439439
if (updateLane !== NoLane) {
440-
if (
441-
enableSyncDefaultUpdates &&
442-
(updateLane === InputContinuousLane ||
443-
updateLane === InputContinuousHydrationLane)
444-
) {
445-
return DefaultLane;
446-
}
447440
return updateLane;
448441
}
449442

@@ -454,13 +447,6 @@ export function requestUpdateLane(fiber: Fiber): Lane {
454447
// use that directly.
455448
// TODO: Move this type conversion to the event priority module.
456449
const eventLane: Lane = (getCurrentEventPriority(): any);
457-
if (
458-
enableSyncDefaultUpdates &&
459-
(eventLane === InputContinuousLane ||
460-
eventLane === InputContinuousHydrationLane)
461-
) {
462-
return DefaultLane;
463-
}
464450
return eventLane;
465451
}
466452

@@ -814,7 +800,9 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
814800
let exitStatus =
815801
enableSyncDefaultUpdates &&
816802
(includesSomeLane(lanes, DefaultLane) ||
817-
includesSomeLane(lanes, DefaultHydrationLane))
803+
includesSomeLane(lanes, InputContinuousLane) ||
804+
includesSomeLane(lanes, DefaultHydrationLane) ||
805+
includesSomeLane(lanes, InputContinuousHydrationLane))
818806
? // Time slicing is disabled for default updates in this root.
819807
renderRootSync(root, lanes)
820808
: renderRootConcurrent(root, lanes);

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

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1405,18 +1405,6 @@ describe('ReactHooksWithNoopRenderer', () => {
14051405
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => {
14061406
setParentState(false);
14071407
});
1408-
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.
1414-
expect(Scheduler).toFlushUntilNextPaint([
1415-
'Child two render',
1416-
'Child one commit',
1417-
'Child two commit',
1418-
]);
1419-
}
14201408
expect(Scheduler).toFlushUntilNextPaint([
14211409
'Parent false render',
14221410
'Parent false commit',

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

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
let React;
22
let ReactNoop;
33
let Scheduler;
4+
let ContinuousEventPriority;
5+
let startTransition;
46
let useState;
57
let useEffect;
68

@@ -11,6 +13,9 @@ describe('ReactUpdatePriority', () => {
1113
React = require('react');
1214
ReactNoop = require('react-noop-renderer');
1315
Scheduler = require('scheduler');
16+
ContinuousEventPriority = require('react-reconciler/constants')
17+
.ContinuousEventPriority;
18+
startTransition = React.unstable_startTransition;
1419
useState = React.useState;
1520
useEffect = React.useEffect;
1621
});
@@ -78,4 +83,53 @@ describe('ReactUpdatePriority', () => {
7883
// Now the idle update has flushed
7984
expect(Scheduler).toHaveYielded(['Idle: 2, Default: 2']);
8085
});
86+
87+
// @gate experimental
88+
test('continuous updates should interrupt transisions', async () => {
89+
const root = ReactNoop.createRoot();
90+
91+
let setCounter;
92+
let setIsHidden;
93+
function App() {
94+
const [counter, _setCounter] = useState(1);
95+
const [isHidden, _setIsHidden] = useState(false);
96+
setCounter = _setCounter;
97+
setIsHidden = _setIsHidden;
98+
if (isHidden) {
99+
return <Text text={'(hidden)'} />;
100+
}
101+
return (
102+
<>
103+
<Text text={'A' + counter} />
104+
<Text text={'B' + counter} />
105+
<Text text={'C' + counter} />
106+
</>
107+
);
108+
}
109+
110+
await ReactNoop.act(async () => {
111+
root.render(<App />);
112+
});
113+
expect(Scheduler).toHaveYielded(['A1', 'B1', 'C1']);
114+
expect(root).toMatchRenderedOutput('A1B1C1');
115+
116+
await ReactNoop.act(async () => {
117+
startTransition(() => {
118+
setCounter(2);
119+
});
120+
expect(Scheduler).toFlushAndYieldThrough(['A2']);
121+
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => {
122+
setIsHidden(true);
123+
});
124+
});
125+
expect(Scheduler).toHaveYielded([
126+
// Because the hide update has continous priority, it should interrupt the
127+
// in-progress transition
128+
'(hidden)',
129+
// When the transition resumes, it's a no-op because the children are
130+
// now hidden.
131+
'(hidden)',
132+
]);
133+
expect(root).toMatchRenderedOutput('(hidden)');
134+
});
81135
});

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

Lines changed: 5 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -168,18 +168,10 @@ describe('SchedulingProfiler labels', () => {
168168
event.initEvent('mouseover', true, true);
169169
dispatchAndSetCurrentEvent(targetRef.current, event);
170170
});
171-
if (gate(flags => flags.enableSyncDefaultUpdates)) {
172-
expect(clearedMarks).toContain(
173-
`--schedule-state-update-${formatLanes(
174-
ReactFiberLane.DefaultLane,
175-
)}-App`,
176-
);
177-
} else {
178-
expect(clearedMarks).toContain(
179-
`--schedule-state-update-${formatLanes(
180-
ReactFiberLane.InputContinuousLane,
181-
)}-App`,
182-
);
183-
}
171+
expect(clearedMarks).toContain(
172+
`--schedule-state-update-${formatLanes(
173+
ReactFiberLane.InputContinuousLane,
174+
)}-App`,
175+
);
184176
});
185177
});

0 commit comments

Comments
 (0)