Skip to content

Commit b48b38a

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

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
@@ -240,6 +240,7 @@ const ceil = Math.ceil;
240240
const {
241241
ReactCurrentDispatcher,
242242
ReactCurrentOwner,
243+
ReactCurrentBatchConfig,
243244
IsSomeRendererActing,
244245
} = ReactSharedInternals;
245246

@@ -1072,11 +1073,14 @@ export function flushDiscreteUpdates() {
10721073

10731074
export function deferredUpdates<A>(fn: () => A): A {
10741075
const previousPriority = getCurrentUpdatePriority();
1076+
const prevTransition = ReactCurrentBatchConfig.transition;
10751077
try {
1078+
ReactCurrentBatchConfig.transition = 0;
10761079
setCurrentUpdatePriority(DefaultEventPriority);
10771080
return fn();
10781081
} finally {
10791082
setCurrentUpdatePriority(previousPriority);
1083+
ReactCurrentBatchConfig.transition = prevTransition;
10801084
}
10811085
}
10821086

@@ -1118,11 +1122,14 @@ export function discreteUpdates<A, B, C, D, R>(
11181122
d: D,
11191123
): R {
11201124
const previousPriority = getCurrentUpdatePriority();
1125+
const prevTransition = ReactCurrentBatchConfig.transition;
11211126
try {
1127+
ReactCurrentBatchConfig.transition = 0;
11221128
setCurrentUpdatePriority(DiscreteEventPriority);
11231129
return fn(a, b, c, d);
11241130
} finally {
11251131
setCurrentUpdatePriority(previousPriority);
1132+
ReactCurrentBatchConfig.transition = prevTransition;
11261133
if (executionContext === NoContext) {
11271134
// Flush the immediate callbacks that were scheduled during this batch
11281135
resetRenderTimer();
@@ -1151,8 +1158,10 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11511158
const prevExecutionContext = executionContext;
11521159
executionContext |= BatchedContext;
11531160

1161+
const prevTransition = ReactCurrentBatchConfig.transition;
11541162
const previousPriority = getCurrentUpdatePriority();
11551163
try {
1164+
ReactCurrentBatchConfig.transition = 0;
11561165
setCurrentUpdatePriority(DiscreteEventPriority);
11571166
if (fn) {
11581167
return fn(a);
@@ -1161,6 +1170,7 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11611170
}
11621171
} finally {
11631172
setCurrentUpdatePriority(previousPriority);
1173+
ReactCurrentBatchConfig.transition = prevTransition;
11641174
executionContext = prevExecutionContext;
11651175
// Flush the immediate callbacks that were scheduled during this batch.
11661176
// Note that this will happen even if batchedUpdates is higher up
@@ -1182,12 +1192,15 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11821192
export function flushControlled(fn: () => mixed): void {
11831193
const prevExecutionContext = executionContext;
11841194
executionContext |= BatchedContext;
1195+
const prevTransition = ReactCurrentBatchConfig.transition;
11851196
const previousPriority = getCurrentUpdatePriority();
11861197
try {
1198+
ReactCurrentBatchConfig.transition = 0;
11871199
setCurrentUpdatePriority(DiscreteEventPriority);
11881200
fn();
11891201
} finally {
11901202
setCurrentUpdatePriority(previousPriority);
1203+
ReactCurrentBatchConfig.transition = prevTransition;
11911204

11921205
executionContext = prevExecutionContext;
11931206
if (executionContext === NoContext) {
@@ -1681,10 +1694,13 @@ function commitRoot(root) {
16811694
// TODO: This no longer makes any sense. We already wrap the mutation and
16821695
// layout phases. Should be able to remove.
16831696
const previousUpdateLanePriority = getCurrentUpdatePriority();
1697+
const prevTransition = ReactCurrentBatchConfig.transition;
16841698
try {
1699+
ReactCurrentBatchConfig.transition = 0;
16851700
setCurrentUpdatePriority(DiscreteEventPriority);
16861701
commitRootImpl(root, previousUpdateLanePriority);
16871702
} finally {
1703+
ReactCurrentBatchConfig.transition = prevTransition;
16881704
setCurrentUpdatePriority(previousUpdateLanePriority);
16891705
}
16901706

@@ -1797,6 +1813,8 @@ function commitRootImpl(root, renderPriorityLevel) {
17971813
NoFlags;
17981814

17991815
if (subtreeHasEffects || rootHasEffect) {
1816+
const prevTransition = ReactCurrentBatchConfig.transition;
1817+
ReactCurrentBatchConfig.transition = 0;
18001818
const previousPriority = getCurrentUpdatePriority();
18011819
setCurrentUpdatePriority(DiscreteEventPriority);
18021820

@@ -1882,6 +1900,7 @@ function commitRootImpl(root, renderPriorityLevel) {
18821900

18831901
// Reset the priority to the previous non-sync value.
18841902
setCurrentUpdatePriority(previousPriority);
1903+
ReactCurrentBatchConfig.transition = prevTransition;
18851904
} else {
18861905
// No effects.
18871906
root.current = finishedWork;
@@ -2019,12 +2038,15 @@ export function flushPassiveEffects(): boolean {
20192038
DefaultEventPriority,
20202039
lanesToEventPriority(pendingPassiveEffectsLanes),
20212040
);
2041+
const prevTransition = ReactCurrentBatchConfig.transition;
20222042
const previousPriority = getCurrentUpdatePriority();
20232043
try {
2044+
ReactCurrentBatchConfig.transition = 0;
20242045
setCurrentUpdatePriority(priority);
20252046
return flushPassiveEffectsImpl();
20262047
} finally {
20272048
setCurrentUpdatePriority(previousPriority);
2049+
ReactCurrentBatchConfig.transition = prevTransition;
20282050
}
20292051
}
20302052
return false;

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

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

@@ -1072,11 +1073,14 @@ export function flushDiscreteUpdates() {
10721073

10731074
export function deferredUpdates<A>(fn: () => A): A {
10741075
const previousPriority = getCurrentUpdatePriority();
1076+
const prevTransition = ReactCurrentBatchConfig.transition;
10751077
try {
1078+
ReactCurrentBatchConfig.transition = 0;
10761079
setCurrentUpdatePriority(DefaultEventPriority);
10771080
return fn();
10781081
} finally {
10791082
setCurrentUpdatePriority(previousPriority);
1083+
ReactCurrentBatchConfig.transition = prevTransition;
10801084
}
10811085
}
10821086

@@ -1118,11 +1122,14 @@ export function discreteUpdates<A, B, C, D, R>(
11181122
d: D,
11191123
): R {
11201124
const previousPriority = getCurrentUpdatePriority();
1125+
const prevTransition = ReactCurrentBatchConfig.transition;
11211126
try {
1127+
ReactCurrentBatchConfig.transition = 0;
11221128
setCurrentUpdatePriority(DiscreteEventPriority);
11231129
return fn(a, b, c, d);
11241130
} finally {
11251131
setCurrentUpdatePriority(previousPriority);
1132+
ReactCurrentBatchConfig.transition = prevTransition;
11261133
if (executionContext === NoContext) {
11271134
// Flush the immediate callbacks that were scheduled during this batch
11281135
resetRenderTimer();
@@ -1151,8 +1158,10 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11511158
const prevExecutionContext = executionContext;
11521159
executionContext |= BatchedContext;
11531160

1161+
const prevTransition = ReactCurrentBatchConfig.transition;
11541162
const previousPriority = getCurrentUpdatePriority();
11551163
try {
1164+
ReactCurrentBatchConfig.transition = 0;
11561165
setCurrentUpdatePriority(DiscreteEventPriority);
11571166
if (fn) {
11581167
return fn(a);
@@ -1161,6 +1170,7 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11611170
}
11621171
} finally {
11631172
setCurrentUpdatePriority(previousPriority);
1173+
ReactCurrentBatchConfig.transition = prevTransition;
11641174
executionContext = prevExecutionContext;
11651175
// Flush the immediate callbacks that were scheduled during this batch.
11661176
// Note that this will happen even if batchedUpdates is higher up
@@ -1182,12 +1192,15 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11821192
export function flushControlled(fn: () => mixed): void {
11831193
const prevExecutionContext = executionContext;
11841194
executionContext |= BatchedContext;
1195+
const prevTransition = ReactCurrentBatchConfig.transition;
11851196
const previousPriority = getCurrentUpdatePriority();
11861197
try {
1198+
ReactCurrentBatchConfig.transition = 0;
11871199
setCurrentUpdatePriority(DiscreteEventPriority);
11881200
fn();
11891201
} finally {
11901202
setCurrentUpdatePriority(previousPriority);
1203+
ReactCurrentBatchConfig.transition = prevTransition;
11911204

11921205
executionContext = prevExecutionContext;
11931206
if (executionContext === NoContext) {
@@ -1681,10 +1694,13 @@ function commitRoot(root) {
16811694
// TODO: This no longer makes any sense. We already wrap the mutation and
16821695
// layout phases. Should be able to remove.
16831696
const previousUpdateLanePriority = getCurrentUpdatePriority();
1697+
const prevTransition = ReactCurrentBatchConfig.transition;
16841698
try {
1699+
ReactCurrentBatchConfig.transition = 0;
16851700
setCurrentUpdatePriority(DiscreteEventPriority);
16861701
commitRootImpl(root, previousUpdateLanePriority);
16871702
} finally {
1703+
ReactCurrentBatchConfig.transition = prevTransition;
16881704
setCurrentUpdatePriority(previousUpdateLanePriority);
16891705
}
16901706

@@ -1797,6 +1813,8 @@ function commitRootImpl(root, renderPriorityLevel) {
17971813
NoFlags;
17981814

17991815
if (subtreeHasEffects || rootHasEffect) {
1816+
const prevTransition = ReactCurrentBatchConfig.transition;
1817+
ReactCurrentBatchConfig.transition = 0;
18001818
const previousPriority = getCurrentUpdatePriority();
18011819
setCurrentUpdatePriority(DiscreteEventPriority);
18021820

@@ -1882,6 +1900,7 @@ function commitRootImpl(root, renderPriorityLevel) {
18821900

18831901
// Reset the priority to the previous non-sync value.
18841902
setCurrentUpdatePriority(previousPriority);
1903+
ReactCurrentBatchConfig.transition = prevTransition;
18851904
} else {
18861905
// No effects.
18871906
root.current = finishedWork;
@@ -2019,12 +2038,15 @@ export function flushPassiveEffects(): boolean {
20192038
DefaultEventPriority,
20202039
lanesToEventPriority(pendingPassiveEffectsLanes),
20212040
);
2041+
const prevTransition = ReactCurrentBatchConfig.transition;
20222042
const previousPriority = getCurrentUpdatePriority();
20232043
try {
2044+
ReactCurrentBatchConfig.transition = 0;
20242045
setCurrentUpdatePriority(priority);
20252046
return flushPassiveEffectsImpl();
20262047
} finally {
20272048
setCurrentUpdatePriority(previousPriority);
2049+
ReactCurrentBatchConfig.transition = prevTransition;
20282050
}
20292051
}
20302052
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)