Skip to content

Commit 2aeb7f9

Browse files
sebmarkbageacdlite
andcommitted
Support nesting of startTransition and flushSync (alt) (facebook#21149)
* Support nesting of startTransition and flushSync * Unset transition before entering any special execution contexts Co-authored-by: Andrew Clark <[email protected]>
1 parent b6a197b commit 2aeb7f9

File tree

4 files changed

+93
-0
lines changed

4 files changed

+93
-0
lines changed

packages/react-dom/src/events/ReactDOMEventListener.js

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,9 @@ import {
5757
getCurrentUpdatePriority,
5858
setCurrentUpdatePriority,
5959
} from 'react-reconciler/src/ReactEventPriorities';
60+
import ReactSharedInternals from 'shared/ReactSharedInternals';
61+
62+
const {ReactCurrentBatchConfig} = ReactSharedInternals;
6063

6164
// TODO: can we stop exporting these?
6265
export let _enabled = true;
@@ -141,11 +144,14 @@ function dispatchContinuousEvent(
141144
nativeEvent,
142145
) {
143146
const previousPriority = getCurrentUpdatePriority();
147+
const prevTransition = ReactCurrentBatchConfig.transition;
148+
ReactCurrentBatchConfig.transition = 0;
144149
try {
145150
setCurrentUpdatePriority(ContinuousEventPriority);
146151
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
147152
} finally {
148153
setCurrentUpdatePriority(previousPriority);
154+
ReactCurrentBatchConfig.transition = prevTransition;
149155
}
150156
}
151157

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ const ceil = Math.ceil;
239239
const {
240240
ReactCurrentDispatcher,
241241
ReactCurrentOwner,
242+
ReactCurrentBatchConfig,
242243
IsSomeRendererActing,
243244
} = ReactSharedInternals;
244245

@@ -1067,11 +1068,14 @@ export function flushDiscreteUpdates() {
10671068

10681069
export function deferredUpdates<A>(fn: () => A): A {
10691070
const previousPriority = getCurrentUpdatePriority();
1071+
const prevTransition = ReactCurrentBatchConfig.transition;
10701072
try {
1073+
ReactCurrentBatchConfig.transition = 0;
10711074
setCurrentUpdatePriority(DefaultEventPriority);
10721075
return fn();
10731076
} finally {
10741077
setCurrentUpdatePriority(previousPriority);
1078+
ReactCurrentBatchConfig.transition = prevTransition;
10751079
}
10761080
}
10771081

@@ -1113,11 +1117,14 @@ export function discreteUpdates<A, B, C, D, R>(
11131117
d: D,
11141118
): R {
11151119
const previousPriority = getCurrentUpdatePriority();
1120+
const prevTransition = ReactCurrentBatchConfig.transition;
11161121
try {
1122+
ReactCurrentBatchConfig.transition = 0;
11171123
setCurrentUpdatePriority(DiscreteEventPriority);
11181124
return fn(a, b, c, d);
11191125
} finally {
11201126
setCurrentUpdatePriority(previousPriority);
1127+
ReactCurrentBatchConfig.transition = prevTransition;
11211128
if (executionContext === NoContext) {
11221129
// Flush the immediate callbacks that were scheduled during this batch
11231130
resetRenderTimer();
@@ -1146,8 +1153,10 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11461153
const prevExecutionContext = executionContext;
11471154
executionContext |= BatchedContext;
11481155

1156+
const prevTransition = ReactCurrentBatchConfig.transition;
11491157
const previousPriority = getCurrentUpdatePriority();
11501158
try {
1159+
ReactCurrentBatchConfig.transition = 0;
11511160
setCurrentUpdatePriority(DiscreteEventPriority);
11521161
if (fn) {
11531162
return fn(a);
@@ -1156,6 +1165,7 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11561165
}
11571166
} finally {
11581167
setCurrentUpdatePriority(previousPriority);
1168+
ReactCurrentBatchConfig.transition = prevTransition;
11591169
executionContext = prevExecutionContext;
11601170
// Flush the immediate callbacks that were scheduled during this batch.
11611171
// Note that this will happen even if batchedUpdates is higher up
@@ -1177,12 +1187,15 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11771187
export function flushControlled(fn: () => mixed): void {
11781188
const prevExecutionContext = executionContext;
11791189
executionContext |= BatchedContext;
1190+
const prevTransition = ReactCurrentBatchConfig.transition;
11801191
const previousPriority = getCurrentUpdatePriority();
11811192
try {
1193+
ReactCurrentBatchConfig.transition = 0;
11821194
setCurrentUpdatePriority(DiscreteEventPriority);
11831195
fn();
11841196
} finally {
11851197
setCurrentUpdatePriority(previousPriority);
1198+
ReactCurrentBatchConfig.transition = prevTransition;
11861199

11871200
executionContext = prevExecutionContext;
11881201
if (executionContext === NoContext) {
@@ -1676,10 +1689,13 @@ function commitRoot(root) {
16761689
// TODO: This no longer makes any sense. We already wrap the mutation and
16771690
// layout phases. Should be able to remove.
16781691
const previousUpdateLanePriority = getCurrentUpdatePriority();
1692+
const prevTransition = ReactCurrentBatchConfig.transition;
16791693
try {
1694+
ReactCurrentBatchConfig.transition = 0;
16801695
setCurrentUpdatePriority(DiscreteEventPriority);
16811696
commitRootImpl(root, previousUpdateLanePriority);
16821697
} finally {
1698+
ReactCurrentBatchConfig.transition = prevTransition;
16831699
setCurrentUpdatePriority(previousUpdateLanePriority);
16841700
}
16851701

@@ -1792,6 +1808,8 @@ function commitRootImpl(root, renderPriorityLevel) {
17921808
NoFlags;
17931809

17941810
if (subtreeHasEffects || rootHasEffect) {
1811+
const prevTransition = ReactCurrentBatchConfig.transition;
1812+
ReactCurrentBatchConfig.transition = 0;
17951813
const previousPriority = getCurrentUpdatePriority();
17961814
setCurrentUpdatePriority(DiscreteEventPriority);
17971815

@@ -1877,6 +1895,7 @@ function commitRootImpl(root, renderPriorityLevel) {
18771895

18781896
// Reset the priority to the previous non-sync value.
18791897
setCurrentUpdatePriority(previousPriority);
1898+
ReactCurrentBatchConfig.transition = prevTransition;
18801899
} else {
18811900
// No effects.
18821901
root.current = finishedWork;
@@ -2018,11 +2037,14 @@ export function flushPassiveEffects(): boolean {
20182037
const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
20192038
const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
20202039
const previousPriority = getCurrentUpdatePriority();
2040+
const prevTransition = ReactCurrentBatchConfig.transition;
20212041
try {
2042+
ReactCurrentBatchConfig.transition = 0;
20222043
setCurrentUpdatePriority(priority);
20232044
return flushPassiveEffectsImpl();
20242045
} finally {
20252046
setCurrentUpdatePriority(previousPriority);
2047+
ReactCurrentBatchConfig.transition = prevTransition;
20262048
}
20272049
}
20282050
return false;

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

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -239,6 +239,7 @@ const ceil = Math.ceil;
239239
const {
240240
ReactCurrentDispatcher,
241241
ReactCurrentOwner,
242+
ReactCurrentBatchConfig,
242243
IsSomeRendererActing,
243244
} = ReactSharedInternals;
244245

@@ -1067,11 +1068,14 @@ export function flushDiscreteUpdates() {
10671068

10681069
export function deferredUpdates<A>(fn: () => A): A {
10691070
const previousPriority = getCurrentUpdatePriority();
1071+
const prevTransition = ReactCurrentBatchConfig.transition;
10701072
try {
1073+
ReactCurrentBatchConfig.transition = 0;
10711074
setCurrentUpdatePriority(DefaultEventPriority);
10721075
return fn();
10731076
} finally {
10741077
setCurrentUpdatePriority(previousPriority);
1078+
ReactCurrentBatchConfig.transition = prevTransition;
10751079
}
10761080
}
10771081

@@ -1113,11 +1117,14 @@ export function discreteUpdates<A, B, C, D, R>(
11131117
d: D,
11141118
): R {
11151119
const previousPriority = getCurrentUpdatePriority();
1120+
const prevTransition = ReactCurrentBatchConfig.transition;
11161121
try {
1122+
ReactCurrentBatchConfig.transition = 0;
11171123
setCurrentUpdatePriority(DiscreteEventPriority);
11181124
return fn(a, b, c, d);
11191125
} finally {
11201126
setCurrentUpdatePriority(previousPriority);
1127+
ReactCurrentBatchConfig.transition = prevTransition;
11211128
if (executionContext === NoContext) {
11221129
// Flush the immediate callbacks that were scheduled during this batch
11231130
resetRenderTimer();
@@ -1146,8 +1153,10 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11461153
const prevExecutionContext = executionContext;
11471154
executionContext |= BatchedContext;
11481155

1156+
const prevTransition = ReactCurrentBatchConfig.transition;
11491157
const previousPriority = getCurrentUpdatePriority();
11501158
try {
1159+
ReactCurrentBatchConfig.transition = 0;
11511160
setCurrentUpdatePriority(DiscreteEventPriority);
11521161
if (fn) {
11531162
return fn(a);
@@ -1156,6 +1165,7 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11561165
}
11571166
} finally {
11581167
setCurrentUpdatePriority(previousPriority);
1168+
ReactCurrentBatchConfig.transition = prevTransition;
11591169
executionContext = prevExecutionContext;
11601170
// Flush the immediate callbacks that were scheduled during this batch.
11611171
// Note that this will happen even if batchedUpdates is higher up
@@ -1177,12 +1187,15 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11771187
export function flushControlled(fn: () => mixed): void {
11781188
const prevExecutionContext = executionContext;
11791189
executionContext |= BatchedContext;
1190+
const prevTransition = ReactCurrentBatchConfig.transition;
11801191
const previousPriority = getCurrentUpdatePriority();
11811192
try {
1193+
ReactCurrentBatchConfig.transition = 0;
11821194
setCurrentUpdatePriority(DiscreteEventPriority);
11831195
fn();
11841196
} finally {
11851197
setCurrentUpdatePriority(previousPriority);
1198+
ReactCurrentBatchConfig.transition = prevTransition;
11861199

11871200
executionContext = prevExecutionContext;
11881201
if (executionContext === NoContext) {
@@ -1676,10 +1689,13 @@ function commitRoot(root) {
16761689
// TODO: This no longer makes any sense. We already wrap the mutation and
16771690
// layout phases. Should be able to remove.
16781691
const previousUpdateLanePriority = getCurrentUpdatePriority();
1692+
const prevTransition = ReactCurrentBatchConfig.transition;
16791693
try {
1694+
ReactCurrentBatchConfig.transition = 0;
16801695
setCurrentUpdatePriority(DiscreteEventPriority);
16811696
commitRootImpl(root, previousUpdateLanePriority);
16821697
} finally {
1698+
ReactCurrentBatchConfig.transition = prevTransition;
16831699
setCurrentUpdatePriority(previousUpdateLanePriority);
16841700
}
16851701

@@ -1792,6 +1808,8 @@ function commitRootImpl(root, renderPriorityLevel) {
17921808
NoFlags;
17931809

17941810
if (subtreeHasEffects || rootHasEffect) {
1811+
const prevTransition = ReactCurrentBatchConfig.transition;
1812+
ReactCurrentBatchConfig.transition = 0;
17951813
const previousPriority = getCurrentUpdatePriority();
17961814
setCurrentUpdatePriority(DiscreteEventPriority);
17971815

@@ -1877,6 +1895,7 @@ function commitRootImpl(root, renderPriorityLevel) {
18771895

18781896
// Reset the priority to the previous non-sync value.
18791897
setCurrentUpdatePriority(previousPriority);
1898+
ReactCurrentBatchConfig.transition = prevTransition;
18801899
} else {
18811900
// No effects.
18821901
root.current = finishedWork;
@@ -2018,11 +2037,14 @@ export function flushPassiveEffects(): boolean {
20182037
const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
20192038
const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
20202039
const previousPriority = getCurrentUpdatePriority();
2040+
const prevTransition = ReactCurrentBatchConfig.transition;
20212041
try {
2042+
ReactCurrentBatchConfig.transition = 0;
20222043
setCurrentUpdatePriority(priority);
20232044
return flushPassiveEffectsImpl();
20242045
} finally {
20252046
setCurrentUpdatePriority(previousPriority);
2047+
ReactCurrentBatchConfig.transition = prevTransition;
20262048
}
20272049
}
20282050
return false;

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

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ let ReactNoop;
33
let Scheduler;
44
let useState;
55
let useEffect;
6+
let startTransition;
67

78
describe('ReactFlushSync', () => {
89
beforeEach(() => {
@@ -13,6 +14,7 @@ describe('ReactFlushSync', () => {
1314
Scheduler = require('scheduler');
1415
useState = React.useState;
1516
useEffect = React.useEffect;
17+
startTransition = React.unstable_startTransition;
1618
});
1719

1820
function Text({text}) {
@@ -54,4 +56,45 @@ describe('ReactFlushSync', () => {
5456
});
5557
expect(root).toMatchRenderedOutput('1, 1');
5658
});
59+
60+
// @gate experimental
61+
test('nested with startTransition', async () => {
62+
let setSyncState;
63+
let setState;
64+
function App() {
65+
const [syncState, _setSyncState] = useState(0);
66+
const [state, _setState] = useState(0);
67+
setSyncState = _setSyncState;
68+
setState = _setState;
69+
return <Text text={`${syncState}, ${state}`} />;
70+
}
71+
72+
const root = ReactNoop.createRoot();
73+
await ReactNoop.act(async () => {
74+
root.render(<App />);
75+
});
76+
expect(Scheduler).toHaveYielded(['0, 0']);
77+
expect(root).toMatchRenderedOutput('0, 0');
78+
79+
await ReactNoop.act(async () => {
80+
ReactNoop.flushSync(() => {
81+
startTransition(() => {
82+
// This should be async even though flushSync is on the stack, because
83+
// startTransition is closer.
84+
setState(1);
85+
ReactNoop.flushSync(() => {
86+
// This should be async even though startTransition is on the stack,
87+
// because flushSync is closer.
88+
setSyncState(1);
89+
});
90+
});
91+
});
92+
// Only the sync update should have flushed
93+
expect(Scheduler).toHaveYielded(['1, 0']);
94+
expect(root).toMatchRenderedOutput('1, 0');
95+
});
96+
// Now the async update has flushed, too.
97+
expect(Scheduler).toHaveYielded(['1, 1']);
98+
expect(root).toMatchRenderedOutput('1, 1');
99+
});
57100
});

0 commit comments

Comments
 (0)