Skip to content

Commit

Permalink
Continuous updates should interrupt transitions
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
acdlite committed Apr 20, 2021
1 parent 1210c37 commit 40fd118
Show file tree
Hide file tree
Showing 7 changed files with 92 additions and 57 deletions.
13 changes: 13 additions & 0 deletions packages/react-reconciler/src/ReactFiberLane.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
enableCache,
enableSchedulingProfiler,
enableUpdaterTracking,
enableSyncDefaultUpdates,
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.new';

Expand Down Expand Up @@ -273,6 +274,18 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
}
}

if (
// TODO: Check for root override, once that lands
enableSyncDefaultUpdates &&
(nextLanes & InputContinuousLane) !== NoLanes
) {
// When updates are sync by default, we entangle continous priority updates
// and default updates, so they render in the same batch. The only reason
// they use separate lanes is because continuous updates should interrupt
// transitions, but default updates should not.
nextLanes |= pendingLanes & DefaultLane;
}

// Check for entangled lanes and add them to the batch.
//
// A lane is said to be entangled with another when it's not allowed to render
Expand Down
13 changes: 13 additions & 0 deletions packages/react-reconciler/src/ReactFiberLane.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@ import {
enableCache,
enableSchedulingProfiler,
enableUpdaterTracking,
enableSyncDefaultUpdates,
} from 'shared/ReactFeatureFlags';
import {isDevToolsPresent} from './ReactFiberDevToolsHook.old';

Expand Down Expand Up @@ -273,6 +274,18 @@ export function getNextLanes(root: FiberRoot, wipLanes: Lanes): Lanes {
}
}

if (
// TODO: Check for root override, once that lands
enableSyncDefaultUpdates &&
(nextLanes & InputContinuousLane) !== NoLanes
) {
// When updates are sync by default, we entangle continous priority updates
// and default updates, so they render in the same batch. The only reason
// they use separate lanes is because continuous updates should interrupt
// transitions, but default updates should not.
nextLanes |= pendingLanes & DefaultLane;
}

// Check for entangled lanes and add them to the batch.
//
// A lane is said to be entangled with another when it's not allowed to render
Expand Down
20 changes: 4 additions & 16 deletions packages/react-reconciler/src/ReactFiberWorkLoop.new.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ import {
NoLanes,
NoLane,
SyncLane,
DefaultLane,
DefaultHydrationLane,
DefaultLane,
InputContinuousLane,
InputContinuousHydrationLane,
NoTimestamp,
Expand Down Expand Up @@ -437,13 +437,6 @@ export function requestUpdateLane(fiber: Fiber): Lane {
// TODO: Move this type conversion to the event priority module.
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
if (
enableSyncDefaultUpdates &&
(updateLane === InputContinuousLane ||
updateLane === InputContinuousHydrationLane)
) {
return DefaultLane;
}
return updateLane;
}

Expand All @@ -454,13 +447,6 @@ export function requestUpdateLane(fiber: Fiber): Lane {
// use that directly.
// TODO: Move this type conversion to the event priority module.
const eventLane: Lane = (getCurrentEventPriority(): any);
if (
enableSyncDefaultUpdates &&
(eventLane === InputContinuousLane ||
eventLane === InputContinuousHydrationLane)
) {
return DefaultLane;
}
return eventLane;
}

Expand Down Expand Up @@ -814,7 +800,9 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
let exitStatus =
enableSyncDefaultUpdates &&
(includesSomeLane(lanes, DefaultLane) ||
includesSomeLane(lanes, DefaultHydrationLane))
includesSomeLane(lanes, InputContinuousLane) ||
includesSomeLane(lanes, DefaultHydrationLane) ||
includesSomeLane(lanes, InputContinuousHydrationLane))
? // Time slicing is disabled for default updates in this root.
renderRootSync(root, lanes)
: renderRootConcurrent(root, lanes);
Expand Down
20 changes: 4 additions & 16 deletions packages/react-reconciler/src/ReactFiberWorkLoop.old.js
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,8 @@ import {
NoLanes,
NoLane,
SyncLane,
DefaultLane,
DefaultHydrationLane,
DefaultLane,
InputContinuousLane,
InputContinuousHydrationLane,
NoTimestamp,
Expand Down Expand Up @@ -437,13 +437,6 @@ export function requestUpdateLane(fiber: Fiber): Lane {
// TODO: Move this type conversion to the event priority module.
const updateLane: Lane = (getCurrentUpdatePriority(): any);
if (updateLane !== NoLane) {
if (
enableSyncDefaultUpdates &&
(updateLane === InputContinuousLane ||
updateLane === InputContinuousHydrationLane)
) {
return DefaultLane;
}
return updateLane;
}

Expand All @@ -454,13 +447,6 @@ export function requestUpdateLane(fiber: Fiber): Lane {
// use that directly.
// TODO: Move this type conversion to the event priority module.
const eventLane: Lane = (getCurrentEventPriority(): any);
if (
enableSyncDefaultUpdates &&
(eventLane === InputContinuousLane ||
eventLane === InputContinuousHydrationLane)
) {
return DefaultLane;
}
return eventLane;
}

Expand Down Expand Up @@ -814,7 +800,9 @@ function performConcurrentWorkOnRoot(root, didTimeout) {
let exitStatus =
enableSyncDefaultUpdates &&
(includesSomeLane(lanes, DefaultLane) ||
includesSomeLane(lanes, DefaultHydrationLane))
includesSomeLane(lanes, InputContinuousLane) ||
includesSomeLane(lanes, DefaultHydrationLane) ||
includesSomeLane(lanes, InputContinuousHydrationLane))
? // Time slicing is disabled for default updates in this root.
renderRootSync(root, lanes)
: renderRootConcurrent(root, lanes);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1405,18 +1405,6 @@ describe('ReactHooksWithNoopRenderer', () => {
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => {
setParentState(false);
});
if (gate(flags => flags.enableSyncDefaultUpdates)) {
// TODO: Default updates do not interrupt transition updates, to
// prevent starvation. However, when sync default updates are enabled,
// continuous updates are treated like default updates. In this case,
// we probably don't want this behavior; continuous should be allowed
// to interrupt.
expect(Scheduler).toFlushUntilNextPaint([
'Child two render',
'Child one commit',
'Child two commit',
]);
}
expect(Scheduler).toFlushUntilNextPaint([
'Parent false render',
'Parent false commit',
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
let React;
let ReactNoop;
let Scheduler;
let ContinuousEventPriority;
let startTransition;
let useState;
let useEffect;

Expand All @@ -11,6 +13,9 @@ describe('ReactUpdatePriority', () => {
React = require('react');
ReactNoop = require('react-noop-renderer');
Scheduler = require('scheduler');
ContinuousEventPriority = require('react-reconciler/constants')
.ContinuousEventPriority;
startTransition = React.unstable_startTransition;
useState = React.useState;
useEffect = React.useEffect;
});
Expand Down Expand Up @@ -78,4 +83,52 @@ describe('ReactUpdatePriority', () => {
// Now the idle update has flushed
expect(Scheduler).toHaveYielded(['Idle: 2, Default: 2']);
});

test('continuous updates should interrupt transisions', async () => {
const root = ReactNoop.createRoot();

let setCounter;
let setIsHidden;
function App() {
const [counter, _setCounter] = useState(1);
const [isHidden, _setIsHidden] = useState(false);
setCounter = _setCounter;
setIsHidden = _setIsHidden;
if (isHidden) {
return <Text text={'(hidden)'} />;
}
return (
<>
<Text text={'A' + counter} />
<Text text={'B' + counter} />
<Text text={'C' + counter} />
</>
);
}

await ReactNoop.act(async () => {
root.render(<App />);
});
expect(Scheduler).toHaveYielded(['A1', 'B1', 'C1']);
expect(root).toMatchRenderedOutput('A1B1C1');

await ReactNoop.act(async () => {
startTransition(() => {
setCounter(2);
});
expect(Scheduler).toFlushAndYieldThrough(['A2']);
ReactNoop.unstable_runWithPriority(ContinuousEventPriority, () => {
setIsHidden(true);
});
});
expect(Scheduler).toHaveYielded([
// Because the hide update has continous priority, it should interrupt the
// in-progress transition
'(hidden)',
// When the transition resumes, it's a no-op because the children are
// now hidden.
'(hidden)',
]);
expect(root).toMatchRenderedOutput('(hidden)');
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -168,18 +168,10 @@ describe('SchedulingProfiler labels', () => {
event.initEvent('mouseover', true, true);
dispatchAndSetCurrentEvent(targetRef.current, event);
});
if (gate(flags => flags.enableSyncDefaultUpdates)) {
expect(clearedMarks).toContain(
`--schedule-state-update-${formatLanes(
ReactFiberLane.DefaultLane,
)}-App`,
);
} else {
expect(clearedMarks).toContain(
`--schedule-state-update-${formatLanes(
ReactFiberLane.InputContinuousLane,
)}-App`,
);
}
expect(clearedMarks).toContain(
`--schedule-state-update-${formatLanes(
ReactFiberLane.InputContinuousLane,
)}-App`,
);
});
});

0 comments on commit 40fd118

Please sign in to comment.