@samitouri / QOS-React / commits / b48b38af68

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 });