Skip to content

Commit aea7c2a

Browse files
committed
Re-land "Support nesting of startTransition and flushSync (alt) (#21149)"
This re-lands commit faa1e12.
1 parent bacc870 commit aea7c2a

File tree

4 files changed

+93
-0
lines changed

4 files changed

+93
-0
lines changed

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

+6
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,9 @@ import {
4949
getCurrentUpdatePriority,
5050
setCurrentUpdatePriority,
5151
} from 'react-reconciler/src/ReactEventPriorities';
52+
import ReactSharedInternals from 'shared/ReactSharedInternals';
53+
54+
const {ReactCurrentBatchConfig} = ReactSharedInternals;
5255

5356
// TODO: can we stop exporting these?
5457
export let _enabled = true;
@@ -125,11 +128,14 @@ function dispatchContinuousEvent(
125128
nativeEvent,
126129
) {
127130
const previousPriority = getCurrentUpdatePriority();
131+
const prevTransition = ReactCurrentBatchConfig.transition;
132+
ReactCurrentBatchConfig.transition = 0;
128133
try {
129134
setCurrentUpdatePriority(ContinuousEventPriority);
130135
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
131136
} finally {
132137
setCurrentUpdatePriority(previousPriority);
138+
ReactCurrentBatchConfig.transition = prevTransition;
133139
}
134140
}
135141

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

+22
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ const ceil = Math.ceil;
245245
const {
246246
ReactCurrentDispatcher,
247247
ReactCurrentOwner,
248+
ReactCurrentBatchConfig,
248249
IsSomeRendererActing,
249250
} = ReactSharedInternals;
250251

@@ -1062,11 +1063,14 @@ export function flushDiscreteUpdates() {
10621063

10631064
export function deferredUpdates<A>(fn: () => A): A {
10641065
const previousPriority = getCurrentUpdatePriority();
1066+
const prevTransition = ReactCurrentBatchConfig.transition;
10651067
try {
1068+
ReactCurrentBatchConfig.transition = 0;
10661069
setCurrentUpdatePriority(DefaultEventPriority);
10671070
return fn();
10681071
} finally {
10691072
setCurrentUpdatePriority(previousPriority);
1073+
ReactCurrentBatchConfig.transition = prevTransition;
10701074
}
10711075
}
10721076

@@ -1110,11 +1114,14 @@ export function discreteUpdates<A, B, C, D, R>(
11101114
d: D,
11111115
): R {
11121116
const previousPriority = getCurrentUpdatePriority();
1117+
const prevTransition = ReactCurrentBatchConfig.transition;
11131118
try {
1119+
ReactCurrentBatchConfig.transition = 0;
11141120
setCurrentUpdatePriority(DiscreteEventPriority);
11151121
return fn(a, b, c, d);
11161122
} finally {
11171123
setCurrentUpdatePriority(previousPriority);
1124+
ReactCurrentBatchConfig.transition = prevTransition;
11181125
if (executionContext === NoContext) {
11191126
resetRenderTimer();
11201127
}
@@ -1144,8 +1151,10 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11441151
const prevExecutionContext = executionContext;
11451152
executionContext |= BatchedContext;
11461153

1154+
const prevTransition = ReactCurrentBatchConfig.transition;
11471155
const previousPriority = getCurrentUpdatePriority();
11481156
try {
1157+
ReactCurrentBatchConfig.transition = 0;
11491158
setCurrentUpdatePriority(DiscreteEventPriority);
11501159
if (fn) {
11511160
return fn(a);
@@ -1154,6 +1163,7 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11541163
}
11551164
} finally {
11561165
setCurrentUpdatePriority(previousPriority);
1166+
ReactCurrentBatchConfig.transition = prevTransition;
11571167
executionContext = prevExecutionContext;
11581168
// Flush the immediate callbacks that were scheduled during this batch.
11591169
// Note that this will happen even if batchedUpdates is higher up
@@ -1175,12 +1185,15 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11751185
export function flushControlled(fn: () => mixed): void {
11761186
const prevExecutionContext = executionContext;
11771187
executionContext |= BatchedContext;
1188+
const prevTransition = ReactCurrentBatchConfig.transition;
11781189
const previousPriority = getCurrentUpdatePriority();
11791190
try {
1191+
ReactCurrentBatchConfig.transition = 0;
11801192
setCurrentUpdatePriority(DiscreteEventPriority);
11811193
fn();
11821194
} finally {
11831195
setCurrentUpdatePriority(previousPriority);
1196+
ReactCurrentBatchConfig.transition = prevTransition;
11841197

11851198
executionContext = prevExecutionContext;
11861199
if (executionContext === NoContext) {
@@ -1675,10 +1688,13 @@ function commitRoot(root) {
16751688
// TODO: This no longer makes any sense. We already wrap the mutation and
16761689
// layout phases. Should be able to remove.
16771690
const previousUpdateLanePriority = getCurrentUpdatePriority();
1691+
const prevTransition = ReactCurrentBatchConfig.transition;
16781692
try {
1693+
ReactCurrentBatchConfig.transition = 0;
16791694
setCurrentUpdatePriority(DiscreteEventPriority);
16801695
commitRootImpl(root, previousUpdateLanePriority);
16811696
} finally {
1697+
ReactCurrentBatchConfig.transition = prevTransition;
16821698
setCurrentUpdatePriority(previousUpdateLanePriority);
16831699
}
16841700

@@ -1800,6 +1816,8 @@ function commitRootImpl(root, renderPriorityLevel) {
18001816
NoFlags;
18011817

18021818
if (subtreeHasEffects || rootHasEffect) {
1819+
const prevTransition = ReactCurrentBatchConfig.transition;
1820+
ReactCurrentBatchConfig.transition = 0;
18031821
const previousPriority = getCurrentUpdatePriority();
18041822
setCurrentUpdatePriority(DiscreteEventPriority);
18051823

@@ -1881,6 +1899,7 @@ function commitRootImpl(root, renderPriorityLevel) {
18811899

18821900
// Reset the priority to the previous non-sync value.
18831901
setCurrentUpdatePriority(previousPriority);
1902+
ReactCurrentBatchConfig.transition = prevTransition;
18841903
} else {
18851904
// No effects.
18861905
root.current = finishedWork;
@@ -2017,12 +2036,15 @@ export function flushPassiveEffects(): boolean {
20172036
if (rootWithPendingPassiveEffects !== null) {
20182037
const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
20192038
const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
2039+
const prevTransition = ReactCurrentBatchConfig.transition;
20202040
const previousPriority = getCurrentUpdatePriority();
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

+22
Original file line numberDiff line numberDiff line change
@@ -245,6 +245,7 @@ const ceil = Math.ceil;
245245
const {
246246
ReactCurrentDispatcher,
247247
ReactCurrentOwner,
248+
ReactCurrentBatchConfig,
248249
IsSomeRendererActing,
249250
} = ReactSharedInternals;
250251

@@ -1062,11 +1063,14 @@ export function flushDiscreteUpdates() {
10621063

10631064
export function deferredUpdates<A>(fn: () => A): A {
10641065
const previousPriority = getCurrentUpdatePriority();
1066+
const prevTransition = ReactCurrentBatchConfig.transition;
10651067
try {
1068+
ReactCurrentBatchConfig.transition = 0;
10661069
setCurrentUpdatePriority(DefaultEventPriority);
10671070
return fn();
10681071
} finally {
10691072
setCurrentUpdatePriority(previousPriority);
1073+
ReactCurrentBatchConfig.transition = prevTransition;
10701074
}
10711075
}
10721076

@@ -1110,11 +1114,14 @@ export function discreteUpdates<A, B, C, D, R>(
11101114
d: D,
11111115
): R {
11121116
const previousPriority = getCurrentUpdatePriority();
1117+
const prevTransition = ReactCurrentBatchConfig.transition;
11131118
try {
1119+
ReactCurrentBatchConfig.transition = 0;
11141120
setCurrentUpdatePriority(DiscreteEventPriority);
11151121
return fn(a, b, c, d);
11161122
} finally {
11171123
setCurrentUpdatePriority(previousPriority);
1124+
ReactCurrentBatchConfig.transition = prevTransition;
11181125
if (executionContext === NoContext) {
11191126
resetRenderTimer();
11201127
}
@@ -1144,8 +1151,10 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11441151
const prevExecutionContext = executionContext;
11451152
executionContext |= BatchedContext;
11461153

1154+
const prevTransition = ReactCurrentBatchConfig.transition;
11471155
const previousPriority = getCurrentUpdatePriority();
11481156
try {
1157+
ReactCurrentBatchConfig.transition = 0;
11491158
setCurrentUpdatePriority(DiscreteEventPriority);
11501159
if (fn) {
11511160
return fn(a);
@@ -1154,6 +1163,7 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11541163
}
11551164
} finally {
11561165
setCurrentUpdatePriority(previousPriority);
1166+
ReactCurrentBatchConfig.transition = prevTransition;
11571167
executionContext = prevExecutionContext;
11581168
// Flush the immediate callbacks that were scheduled during this batch.
11591169
// Note that this will happen even if batchedUpdates is higher up
@@ -1175,12 +1185,15 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
11751185
export function flushControlled(fn: () => mixed): void {
11761186
const prevExecutionContext = executionContext;
11771187
executionContext |= BatchedContext;
1188+
const prevTransition = ReactCurrentBatchConfig.transition;
11781189
const previousPriority = getCurrentUpdatePriority();
11791190
try {
1191+
ReactCurrentBatchConfig.transition = 0;
11801192
setCurrentUpdatePriority(DiscreteEventPriority);
11811193
fn();
11821194
} finally {
11831195
setCurrentUpdatePriority(previousPriority);
1196+
ReactCurrentBatchConfig.transition = prevTransition;
11841197

11851198
executionContext = prevExecutionContext;
11861199
if (executionContext === NoContext) {
@@ -1675,10 +1688,13 @@ function commitRoot(root) {
16751688
// TODO: This no longer makes any sense. We already wrap the mutation and
16761689
// layout phases. Should be able to remove.
16771690
const previousUpdateLanePriority = getCurrentUpdatePriority();
1691+
const prevTransition = ReactCurrentBatchConfig.transition;
16781692
try {
1693+
ReactCurrentBatchConfig.transition = 0;
16791694
setCurrentUpdatePriority(DiscreteEventPriority);
16801695
commitRootImpl(root, previousUpdateLanePriority);
16811696
} finally {
1697+
ReactCurrentBatchConfig.transition = prevTransition;
16821698
setCurrentUpdatePriority(previousUpdateLanePriority);
16831699
}
16841700

@@ -1800,6 +1816,8 @@ function commitRootImpl(root, renderPriorityLevel) {
18001816
NoFlags;
18011817

18021818
if (subtreeHasEffects || rootHasEffect) {
1819+
const prevTransition = ReactCurrentBatchConfig.transition;
1820+
ReactCurrentBatchConfig.transition = 0;
18031821
const previousPriority = getCurrentUpdatePriority();
18041822
setCurrentUpdatePriority(DiscreteEventPriority);
18051823

@@ -1881,6 +1899,7 @@ function commitRootImpl(root, renderPriorityLevel) {
18811899

18821900
// Reset the priority to the previous non-sync value.
18831901
setCurrentUpdatePriority(previousPriority);
1902+
ReactCurrentBatchConfig.transition = prevTransition;
18841903
} else {
18851904
// No effects.
18861905
root.current = finishedWork;
@@ -2017,12 +2036,15 @@ export function flushPassiveEffects(): boolean {
20172036
if (rootWithPendingPassiveEffects !== null) {
20182037
const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
20192038
const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
2039+
const prevTransition = ReactCurrentBatchConfig.transition;
20202040
const previousPriority = getCurrentUpdatePriority();
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

+43
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}) {
@@ -62,6 +64,47 @@ describe('ReactFlushSync', () => {
6264
expect(root).toMatchRenderedOutput('1, 1');
6365
});
6466

67+
// @gate experimental
68+
test('nested with startTransition', async () => {
69+
let setSyncState;
70+
let setState;
71+
function App() {
72+
const [syncState, _setSyncState] = useState(0);
73+
const [state, _setState] = useState(0);
74+
setSyncState = _setSyncState;
75+
setState = _setState;
76+
return <Text text={`${syncState}, ${state}`} />;
77+
}
78+
79+
const root = ReactNoop.createRoot();
80+
await ReactNoop.act(async () => {
81+
root.render(<App />);
82+
});
83+
expect(Scheduler).toHaveYielded(['0, 0']);
84+
expect(root).toMatchRenderedOutput('0, 0');
85+
86+
await ReactNoop.act(async () => {
87+
ReactNoop.flushSync(() => {
88+
startTransition(() => {
89+
// This should be async even though flushSync is on the stack, because
90+
// startTransition is closer.
91+
setState(1);
92+
ReactNoop.flushSync(() => {
93+
// This should be async even though startTransition is on the stack,
94+
// because flushSync is closer.
95+
setSyncState(1);
96+
});
97+
});
98+
});
99+
// Only the sync update should have flushed
100+
expect(Scheduler).toHaveYielded(['1, 0']);
101+
expect(root).toMatchRenderedOutput('1, 0');
102+
});
103+
// Now the async update has flushed, too.
104+
expect(Scheduler).toHaveYielded(['1, 1']);
105+
expect(root).toMatchRenderedOutput('1, 1');
106+
});
107+
65108
test('flushes passive effects synchronously when they are the result of a sync render', async () => {
66109
function App() {
67110
useEffect(() => {

0 commit comments

Comments
 (0)