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 <git@andrewclark.io>
Sebastian Markbåge committed
Mar 31, 2021 at 11:22 UTC
b48b38af68c27fd401fe4b923a8fa0b229693cd4
4 files changed
+93
packages/react-dom/src/events/ReactDOMEventListener.js
+6
@@ -57,6 +57,9 @@ import {
57
getCurrentUpdatePriority,
58
setCurrentUpdatePriority,
59
} from 'react-reconciler/src/ReactEventPriorities';
60
+import ReactSharedInternals from 'shared/ReactSharedInternals';
61
+
62
+const {ReactCurrentBatchConfig} = ReactSharedInternals;
63
64
// TODO: can we stop exporting these?
65
export let _enabled = true;
@@ -141,11 +144,14 @@ function dispatchContinuousEvent(
144
nativeEvent,
145
) {
146
const previousPriority = getCurrentUpdatePriority();
147
+ const prevTransition = ReactCurrentBatchConfig.transition;
148
+ ReactCurrentBatchConfig.transition = 0;
149
try {
150
setCurrentUpdatePriority(ContinuousEventPriority);
151
dispatchEvent(domEventName, eventSystemFlags, container, nativeEvent);
152
} finally {
153
setCurrentUpdatePriority(previousPriority);
154
+ ReactCurrentBatchConfig.transition = prevTransition;
155
}
156
}
157
packages/react-reconciler/src/ReactFiberWorkLoop.new.js
+22
@@ -240,6 +240,7 @@ const ceil = Math.ceil;
240
const {
241
ReactCurrentDispatcher,
242
ReactCurrentOwner,
243
+ ReactCurrentBatchConfig,
244
IsSomeRendererActing,
245
} = ReactSharedInternals;
246
@@ -1072,11 +1073,14 @@ export function flushDiscreteUpdates() {
1073
1074
export function deferredUpdates<A>(fn: () => A): A {
1075
const previousPriority = getCurrentUpdatePriority();
1076
+ const prevTransition = ReactCurrentBatchConfig.transition;
1077
try {
1078
+ ReactCurrentBatchConfig.transition = 0;
1079
setCurrentUpdatePriority(DefaultEventPriority);
1080
return fn();
1081
} finally {
1082
setCurrentUpdatePriority(previousPriority);
1083
+ ReactCurrentBatchConfig.transition = prevTransition;
1084
}
1085
}
1086
@@ -1118,11 +1122,14 @@ export function discreteUpdates<A, B, C, D, R>(
1122
d: D,
1123
): R {
1124
const previousPriority = getCurrentUpdatePriority();
1125
+ const prevTransition = ReactCurrentBatchConfig.transition;
1126
try {
1127
+ ReactCurrentBatchConfig.transition = 0;
1128
setCurrentUpdatePriority(DiscreteEventPriority);
1129
return fn(a, b, c, d);
1130
} finally {
1131
setCurrentUpdatePriority(previousPriority);
1132
+ ReactCurrentBatchConfig.transition = prevTransition;
1133
if (executionContext === NoContext) {
1134
// Flush the immediate callbacks that were scheduled during this batch
1135
resetRenderTimer();
@@ -1151,8 +1158,10 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
1158
const prevExecutionContext = executionContext;
1159
executionContext |= BatchedContext;
1160
1161
+ const prevTransition = ReactCurrentBatchConfig.transition;
1162
const previousPriority = getCurrentUpdatePriority();
1163
try {
1164
+ ReactCurrentBatchConfig.transition = 0;
1165
setCurrentUpdatePriority(DiscreteEventPriority);
1166
if (fn) {
1167
return fn(a);
@@ -1161,6 +1170,7 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
1170
}
1171
} finally {
1172
setCurrentUpdatePriority(previousPriority);
1173
+ ReactCurrentBatchConfig.transition = prevTransition;
1174
executionContext = prevExecutionContext;
1175
// Flush the immediate callbacks that were scheduled during this batch.
1176
// 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 {
1192
export function flushControlled(fn: () => mixed): void {
1193
const prevExecutionContext = executionContext;
1194
executionContext |= BatchedContext;
1195
+ const prevTransition = ReactCurrentBatchConfig.transition;
1196
const previousPriority = getCurrentUpdatePriority();
1197
try {
1198
+ ReactCurrentBatchConfig.transition = 0;
1199
setCurrentUpdatePriority(DiscreteEventPriority);
1200
fn();
1201
} finally {
1202
setCurrentUpdatePriority(previousPriority);
1203
+ ReactCurrentBatchConfig.transition = prevTransition;
1204
1205
executionContext = prevExecutionContext;
1206
if (executionContext === NoContext) {
@@ -1681,10 +1694,13 @@ function commitRoot(root) {
1694
// TODO: This no longer makes any sense. We already wrap the mutation and
1695
// layout phases. Should be able to remove.
1696
const previousUpdateLanePriority = getCurrentUpdatePriority();
1697
+ const prevTransition = ReactCurrentBatchConfig.transition;
1698
try {
1699
+ ReactCurrentBatchConfig.transition = 0;
1700
setCurrentUpdatePriority(DiscreteEventPriority);
1701
commitRootImpl(root, previousUpdateLanePriority);
1702
} finally {
1703
+ ReactCurrentBatchConfig.transition = prevTransition;
1704
setCurrentUpdatePriority(previousUpdateLanePriority);
1705
}
1706
@@ -1797,6 +1813,8 @@ function commitRootImpl(root, renderPriorityLevel) {
1813
NoFlags;
1814
1815
if (subtreeHasEffects || rootHasEffect) {
1816
+ const prevTransition = ReactCurrentBatchConfig.transition;
1817
+ ReactCurrentBatchConfig.transition = 0;
1818
const previousPriority = getCurrentUpdatePriority();
1819
setCurrentUpdatePriority(DiscreteEventPriority);
1820
@@ -1882,6 +1900,7 @@ function commitRootImpl(root, renderPriorityLevel) {
1900
1901
// Reset the priority to the previous non-sync value.
1902
setCurrentUpdatePriority(previousPriority);
1903
+ ReactCurrentBatchConfig.transition = prevTransition;
1904
} else {
1905
// No effects.
1906
root.current = finishedWork;
@@ -2019,12 +2038,15 @@ export function flushPassiveEffects(): boolean {
2038
DefaultEventPriority,
2039
lanesToEventPriority(pendingPassiveEffectsLanes),
2040
);
2041
+ const prevTransition = ReactCurrentBatchConfig.transition;
2042
const previousPriority = getCurrentUpdatePriority();
2043
try {
2044
+ ReactCurrentBatchConfig.transition = 0;
2045
setCurrentUpdatePriority(priority);
2046
return flushPassiveEffectsImpl();
2047
} finally {
2048
setCurrentUpdatePriority(previousPriority);
2049
+ ReactCurrentBatchConfig.transition = prevTransition;
2050
}
2051
}
2052
return false;
packages/react-reconciler/src/ReactFiberWorkLoop.old.js
+22
@@ -240,6 +240,7 @@ const ceil = Math.ceil;
240
const {
241
ReactCurrentDispatcher,
242
ReactCurrentOwner,
243
+ ReactCurrentBatchConfig,
244
IsSomeRendererActing,
245
} = ReactSharedInternals;
246
@@ -1072,11 +1073,14 @@ export function flushDiscreteUpdates() {
1073
1074
export function deferredUpdates<A>(fn: () => A): A {
1075
const previousPriority = getCurrentUpdatePriority();
1076
+ const prevTransition = ReactCurrentBatchConfig.transition;
1077
try {
1078
+ ReactCurrentBatchConfig.transition = 0;
1079
setCurrentUpdatePriority(DefaultEventPriority);
1080
return fn();
1081
} finally {
1082
setCurrentUpdatePriority(previousPriority);
1083
+ ReactCurrentBatchConfig.transition = prevTransition;
1084
}
1085
}
1086
@@ -1118,11 +1122,14 @@ export function discreteUpdates<A, B, C, D, R>(
1122
d: D,
1123
): R {
1124
const previousPriority = getCurrentUpdatePriority();
1125
+ const prevTransition = ReactCurrentBatchConfig.transition;
1126
try {
1127
+ ReactCurrentBatchConfig.transition = 0;
1128
setCurrentUpdatePriority(DiscreteEventPriority);
1129
return fn(a, b, c, d);
1130
} finally {
1131
setCurrentUpdatePriority(previousPriority);
1132
+ ReactCurrentBatchConfig.transition = prevTransition;
1133
if (executionContext === NoContext) {
1134
// Flush the immediate callbacks that were scheduled during this batch
1135
resetRenderTimer();
@@ -1151,8 +1158,10 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
1158
const prevExecutionContext = executionContext;
1159
executionContext |= BatchedContext;
1160
1161
+ const prevTransition = ReactCurrentBatchConfig.transition;
1162
const previousPriority = getCurrentUpdatePriority();
1163
try {
1164
+ ReactCurrentBatchConfig.transition = 0;
1165
setCurrentUpdatePriority(DiscreteEventPriority);
1166
if (fn) {
1167
return fn(a);
@@ -1161,6 +1170,7 @@ export function flushSync<A, R>(fn: A => R, a: A): R {
1170
}
1171
} finally {
1172
setCurrentUpdatePriority(previousPriority);
1173
+ ReactCurrentBatchConfig.transition = prevTransition;
1174
executionContext = prevExecutionContext;
1175
// Flush the immediate callbacks that were scheduled during this batch.
1176
// 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 {
1192
export function flushControlled(fn: () => mixed): void {
1193
const prevExecutionContext = executionContext;
1194
executionContext |= BatchedContext;
1195
+ const prevTransition = ReactCurrentBatchConfig.transition;
1196
const previousPriority = getCurrentUpdatePriority();
1197
try {
1198
+ ReactCurrentBatchConfig.transition = 0;
1199
setCurrentUpdatePriority(DiscreteEventPriority);
1200
fn();
1201
} finally {
1202
setCurrentUpdatePriority(previousPriority);
1203
+ ReactCurrentBatchConfig.transition = prevTransition;
1204
1205
executionContext = prevExecutionContext;
1206
if (executionContext === NoContext) {
@@ -1681,10 +1694,13 @@ function commitRoot(root) {
1694
// TODO: This no longer makes any sense. We already wrap the mutation and
1695
// layout phases. Should be able to remove.
1696
const previousUpdateLanePriority = getCurrentUpdatePriority();
1697
+ const prevTransition = ReactCurrentBatchConfig.transition;
1698
try {
1699
+ ReactCurrentBatchConfig.transition = 0;
1700
setCurrentUpdatePriority(DiscreteEventPriority);
1701
commitRootImpl(root, previousUpdateLanePriority);
1702
} finally {
1703
+ ReactCurrentBatchConfig.transition = prevTransition;
1704
setCurrentUpdatePriority(previousUpdateLanePriority);
1705
}
1706
@@ -1797,6 +1813,8 @@ function commitRootImpl(root, renderPriorityLevel) {
1813
NoFlags;
1814
1815
if (subtreeHasEffects || rootHasEffect) {
1816
+ const prevTransition = ReactCurrentBatchConfig.transition;
1817
+ ReactCurrentBatchConfig.transition = 0;
1818
const previousPriority = getCurrentUpdatePriority();
1819
setCurrentUpdatePriority(DiscreteEventPriority);
1820
@@ -1882,6 +1900,7 @@ function commitRootImpl(root, renderPriorityLevel) {
1900
1901
// Reset the priority to the previous non-sync value.
1902
setCurrentUpdatePriority(previousPriority);
1903
+ ReactCurrentBatchConfig.transition = prevTransition;
1904
} else {
1905
// No effects.
1906
root.current = finishedWork;
@@ -2019,12 +2038,15 @@ export function flushPassiveEffects(): boolean {
2038
DefaultEventPriority,
2039
lanesToEventPriority(pendingPassiveEffectsLanes),
2040
);
2041
+ const prevTransition = ReactCurrentBatchConfig.transition;
2042
const previousPriority = getCurrentUpdatePriority();
2043
try {
2044
+ ReactCurrentBatchConfig.transition = 0;
2045
setCurrentUpdatePriority(priority);
2046
return flushPassiveEffectsImpl();
2047
} finally {
2048
setCurrentUpdatePriority(previousPriority);
2049
+ ReactCurrentBatchConfig.transition = prevTransition;
2050
}
2051
}
2052
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}) {
@@ -54,4 +56,45 @@ describe('ReactFlushSync', () => {
56
});
57
expect(root).toMatchRenderedOutput('1, 1');
58
});
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
+ });
100
});