Re-land "Support nesting of startTransition and flushSync (alt) (#21149)"
This re-lands commit faa1e127f1ba755da846bc6ce299cdefaf97721f.
Andrew Clark committed
May 3, 2021 at 13:34 UTC
aea7c2aab18f2c288a1fcab0cf7a14560b5db35e
4 files changed
+93
packages/react-dom/src/events/ReactDOMEventListener.js
+6
@@ -49,6 +49,9 @@ import {
49
getCurrentUpdatePriority,
50
setCurrentUpdatePriority,
51
} from 'react-reconciler/src/ReactEventPriorities';
52
+import ReactSharedInternals from 'shared/ReactSharedInternals';
53
+
54
+const {ReactCurrentBatchConfig} = ReactSharedInternals;
55
56
// TODO: can we stop exporting these?
57
export let _enabled = true;
@@ -125,11 +128,14 @@ function dispatchContinuousEvent(
128
nativeEvent,
129
) {
130
const previousPriority = getCurrentUpdatePriority();
131
+ const prevTransition = ReactCurrentBatchConfig.transition;
132
+ ReactCurrentBatchConfig.transition = 0;
133
try {
134
setCurrentUpdatePriority(ContinuousEventPriority);
135
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
136
} finally {
137
setCurrentUpdatePriority(previousPriority);
138
+ ReactCurrentBatchConfig.transition = prevTransition;
139
}
140
}
141
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+22
@@ -245,6 +245,7 @@ const ceil = Math.ceil;
245
const {
246
ReactCurrentDispatcher,
247
ReactCurrentOwner,
248
+ ReactCurrentBatchConfig,
249
IsSomeRendererActing,
250
} = ReactSharedInternals;
251
@@ -1062,11 +1063,14 @@ export function flushDiscreteUpdates() {
1063
1064
export function deferredUpdates<A>(fn: () => A): A {
1065
const previousPriority = getCurrentUpdatePriority();
1066
+ const prevTransition = ReactCurrentBatchConfig.transition;
1067
try {
1068
+ ReactCurrentBatchConfig.transition = 0;
1069
setCurrentUpdatePriority(DefaultEventPriority);
1070
return fn();
1071
} finally {
1072
setCurrentUpdatePriority(previousPriority);
1073
+ ReactCurrentBatchConfig.transition = prevTransition;
1074
}
1075
}
1076
@@ -1110,11 +1114,14 @@ export function discreteUpdates<A, B, C, D, R>(
1114
d: D,
1115
): R {
1116
const previousPriority = getCurrentUpdatePriority();
1117
+ const prevTransition = ReactCurrentBatchConfig.transition;
1118
try {
1119
+ ReactCurrentBatchConfig.transition = 0;
1120
setCurrentUpdatePriority(DiscreteEventPriority);
1121
return fn(a, b, c, d);
1122
} finally {
1123
setCurrentUpdatePriority(previousPriority);
1124
+ ReactCurrentBatchConfig.transition = prevTransition;
1125
if (executionContext === NoContext) {
1126
resetRenderTimer();
1127
}
@@ -1144,8 +1151,10 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
1151
const prevExecutionContext = executionContext;
1152
executionContext |= BatchedContext;
1153
1154
+ const prevTransition = ReactCurrentBatchConfig.transition;
1155
const previousPriority = getCurrentUpdatePriority();
1156
try {
1157
+ ReactCurrentBatchConfig.transition = 0;
1158
setCurrentUpdatePriority(DiscreteEventPriority);
1159
if (fn) {
1160
return fn(a);
@@ -1154,6 +1163,7 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
1163
}
1164
} finally {
1165
setCurrentUpdatePriority(previousPriority);
1166
+ ReactCurrentBatchConfig.transition = prevTransition;
1167
executionContext = prevExecutionContext;
1168
// Flush the immediate callbacks that were scheduled during this batch.
1169
// 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 {
1185
export function flushControlled(fn: () => mixed): void {
1186
const prevExecutionContext = executionContext;
1187
executionContext |= BatchedContext;
1188
+ const prevTransition = ReactCurrentBatchConfig.transition;
1189
const previousPriority = getCurrentUpdatePriority();
1190
try {
1191
+ ReactCurrentBatchConfig.transition = 0;
1192
setCurrentUpdatePriority(DiscreteEventPriority);
1193
fn();
1194
} finally {
1195
setCurrentUpdatePriority(previousPriority);
1196
+ ReactCurrentBatchConfig.transition = prevTransition;
1197
1198
executionContext = prevExecutionContext;
1199
if (executionContext === NoContext) {
@@ -1675,10 +1688,13 @@ function commitRoot(root) {
1688
// TODO: This no longer makes any sense. We already wrap the mutation and
1689
// layout phases. Should be able to remove.
1690
const previousUpdateLanePriority = getCurrentUpdatePriority();
1691
+ const prevTransition = ReactCurrentBatchConfig.transition;
1692
try {
1693
+ ReactCurrentBatchConfig.transition = 0;
1694
setCurrentUpdatePriority(DiscreteEventPriority);
1695
commitRootImpl(root, previousUpdateLanePriority);
1696
} finally {
1697
+ ReactCurrentBatchConfig.transition = prevTransition;
1698
setCurrentUpdatePriority(previousUpdateLanePriority);
1699
}
1700
@@ -1800,6 +1816,8 @@ function commitRootImpl(root, renderPriorityLevel) {
1816
NoFlags;
1817
1818
if (subtreeHasEffects || rootHasEffect) {
1819
+ const prevTransition = ReactCurrentBatchConfig.transition;
1820
+ ReactCurrentBatchConfig.transition = 0;
1821
const previousPriority = getCurrentUpdatePriority();
1822
setCurrentUpdatePriority(DiscreteEventPriority);
1823
@@ -1881,6 +1899,7 @@ function commitRootImpl(root, renderPriorityLevel) {
1899
1900
// Reset the priority to the previous non-sync value.
1901
setCurrentUpdatePriority(previousPriority);
1902
+ ReactCurrentBatchConfig.transition = prevTransition;
1903
} else {
1904
// No effects.
1905
root.current = finishedWork;
@@ -2017,12 +2036,15 @@ export function flushPassiveEffects(): boolean {
2036
if (rootWithPendingPassiveEffects !== null) {
2037
const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
2038
const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
2039
+ const prevTransition = ReactCurrentBatchConfig.transition;
2040
const previousPriority = getCurrentUpdatePriority();
2041
try {
2042
+ ReactCurrentBatchConfig.transition = 0;
2043
setCurrentUpdatePriority(priority);
2044
return flushPassiveEffectsImpl();
2045
} finally {
2046
setCurrentUpdatePriority(previousPriority);
2047
+ ReactCurrentBatchConfig.transition = prevTransition;
2048
}
2049
}
2050
return false;
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+22
@@ -245,6 +245,7 @@ const ceil = Math.ceil;
245
const {
246
ReactCurrentDispatcher,
247
ReactCurrentOwner,
248
+ ReactCurrentBatchConfig,
249
IsSomeRendererActing,
250
} = ReactSharedInternals;
251
@@ -1062,11 +1063,14 @@ export function flushDiscreteUpdates() {
1063
1064
export function deferredUpdates<A>(fn: () => A): A {
1065
const previousPriority = getCurrentUpdatePriority();
1066
+ const prevTransition = ReactCurrentBatchConfig.transition;
1067
try {
1068
+ ReactCurrentBatchConfig.transition = 0;
1069
setCurrentUpdatePriority(DefaultEventPriority);
1070
return fn();
1071
} finally {
1072
setCurrentUpdatePriority(previousPriority);
1073
+ ReactCurrentBatchConfig.transition = prevTransition;
1074
}
1075
}
1076
@@ -1110,11 +1114,14 @@ export function discreteUpdates<A, B, C, D, R>(
1114
d: D,
1115
): R {
1116
const previousPriority = getCurrentUpdatePriority();
1117
+ const prevTransition = ReactCurrentBatchConfig.transition;
1118
try {
1119
+ ReactCurrentBatchConfig.transition = 0;
1120
setCurrentUpdatePriority(DiscreteEventPriority);
1121
return fn(a, b, c, d);
1122
} finally {
1123
setCurrentUpdatePriority(previousPriority);
1124
+ ReactCurrentBatchConfig.transition = prevTransition;
1125
if (executionContext === NoContext) {
1126
resetRenderTimer();
1127
}
@@ -1144,8 +1151,10 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
1151
const prevExecutionContext = executionContext;
1152
executionContext |= BatchedContext;
1153
1154
+ const prevTransition = ReactCurrentBatchConfig.transition;
1155
const previousPriority = getCurrentUpdatePriority();
1156
try {
1157
+ ReactCurrentBatchConfig.transition = 0;
1158
setCurrentUpdatePriority(DiscreteEventPriority);
1159
if (fn) {
1160
return fn(a);
@@ -1154,6 +1163,7 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
1163
}
1164
} finally {
1165
setCurrentUpdatePriority(previousPriority);
1166
+ ReactCurrentBatchConfig.transition = prevTransition;
1167
executionContext = prevExecutionContext;
1168
// Flush the immediate callbacks that were scheduled during this batch.
1169
// 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 {
1185
export function flushControlled(fn: () => mixed): void {
1186
const prevExecutionContext = executionContext;
1187
executionContext |= BatchedContext;
1188
+ const prevTransition = ReactCurrentBatchConfig.transition;
1189
const previousPriority = getCurrentUpdatePriority();
1190
try {
1191
+ ReactCurrentBatchConfig.transition = 0;
1192
setCurrentUpdatePriority(DiscreteEventPriority);
1193
fn();
1194
} finally {
1195
setCurrentUpdatePriority(previousPriority);
1196
+ ReactCurrentBatchConfig.transition = prevTransition;
1197
1198
executionContext = prevExecutionContext;
1199
if (executionContext === NoContext) {
@@ -1675,10 +1688,13 @@ function commitRoot(root) {
1688
// TODO: This no longer makes any sense. We already wrap the mutation and
1689
// layout phases. Should be able to remove.
1690
const previousUpdateLanePriority = getCurrentUpdatePriority();
1691
+ const prevTransition = ReactCurrentBatchConfig.transition;
1692
try {
1693
+ ReactCurrentBatchConfig.transition = 0;
1694
setCurrentUpdatePriority(DiscreteEventPriority);
1695
commitRootImpl(root, previousUpdateLanePriority);
1696
} finally {
1697
+ ReactCurrentBatchConfig.transition = prevTransition;
1698
setCurrentUpdatePriority(previousUpdateLanePriority);
1699
}
1700
@@ -1800,6 +1816,8 @@ function commitRootImpl(root, renderPriorityLevel) {
1816
NoFlags;
1817
1818
if (subtreeHasEffects || rootHasEffect) {
1819
+ const prevTransition = ReactCurrentBatchConfig.transition;
1820
+ ReactCurrentBatchConfig.transition = 0;
1821
const previousPriority = getCurrentUpdatePriority();
1822
setCurrentUpdatePriority(DiscreteEventPriority);
1823
@@ -1881,6 +1899,7 @@ function commitRootImpl(root, renderPriorityLevel) {
1899
1900
// Reset the priority to the previous non-sync value.
1901
setCurrentUpdatePriority(previousPriority);
1902
+ ReactCurrentBatchConfig.transition = prevTransition;
1903
} else {
1904
// No effects.
1905
root.current = finishedWork;
@@ -2017,12 +2036,15 @@ export function flushPassiveEffects(): boolean {
2036
if (rootWithPendingPassiveEffects !== null) {
2037
const renderPriority = lanesToEventPriority(pendingPassiveEffectsLanes);
2038
const priority = lowerEventPriority(DefaultEventPriority, renderPriority);
2039
+ const prevTransition = ReactCurrentBatchConfig.transition;
2040
const previousPriority = getCurrentUpdatePriority();
2041
try {
2042
+ ReactCurrentBatchConfig.transition = 0;
2043
setCurrentUpdatePriority(priority);
2044
return flushPassiveEffectsImpl();
2045
} finally {
2046
setCurrentUpdatePriority(previousPriority);
2047
+ ReactCurrentBatchConfig.transition = prevTransition;
2048
}
2049
}
2050
return false;
packages/react-reconciler/src/__tests__/ReactFlushSync-test.js
+43
@@ -3,6 +3,7 @@ let ReactNoop;
3
let Scheduler;
4
let useState;
5
let useEffect;
6
+let startTransition;
7
8
describe('ReactFlushSync', () => {
9
beforeEach(() => {
@@ -13,6 +14,7 @@ describe('ReactFlushSync', () => {
14
Scheduler = require('scheduler');
15
useState = React.useState;
16
useEffect = React.useEffect;
17
+ startTransition = React.unstable_startTransition;
18
});
19
20
function Text({text}) {
@@ -62,6 +64,47 @@ describe('ReactFlushSync', () => {
64
expect(root).toMatchRenderedOutput('1, 1');
65
});
66
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
+
108
test('flushes passive effects synchronously when they are the result of a sync render', async () => {
109
function App() {
110
useEffect(() => {