Skip to content

Commit fc578ff

Browse files
committed
Fix async batching in React.startTransition (#29226)
Note: Despite the similar-sounding description, this fix is unrelated to the issue where updates that occur after an `await` in an async action must also be wrapped in their own `startTransition`, due to the absence of an AsyncContext mechanism in browsers today. --- Discovered a flaw in the current implementation of the isomorphic startTransition implementation (React.startTransition), related to async actions. It only creates an async scope if something calls setState within the synchronous part of the action (i.e. before the first `await`). I had thought this was fine because if there's no update during this part, then there's nothing that needs to be entangled. I didn't think this through, though — if there are multiple async updates interleaved throughout the rest of the action, we need the async scope to have already been created so that _those_ are batched together. An even easier way to observe this is to schedule an optimistic update after an `await` — the optimistic update should not be reverted until the async action is complete. To implement, during the reconciler's module initialization, we compose its startTransition implementation with any previous reconciler's startTransition that was already initialized. Then, the isomorphic startTransition is the composition of every reconciler's startTransition. ```js function startTransition(fn) { return startTransitionDOM(() => { return startTransitionART(() => { return startTransitionThreeFiber(() => { // and so on... return fn(); }); }); }); } ``` This is basically how flushSync is implemented, too. DiffTrain build for commit ee5c194.
1 parent 5be2fe2 commit fc578ff

File tree

13 files changed

+326
-285
lines changed

13 files changed

+326
-285
lines changed

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-test-renderer/cjs/ReactTestRenderer-dev.js

Lines changed: 50 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<7d577e8126a68c701e64111a9f2bbd2a>>
10+
* @generated SignedSource<<8b9b9798442c5728adfabb7cd8ca5f24>>
1111
*/
1212

1313
'use strict';
@@ -8576,9 +8576,7 @@ function runActionStateAction(actionQueue, setPendingState, setState, payload) {
85768576
var prevState = actionQueue.state; // This is a fork of startTransition
85778577

85788578
var prevTransition = ReactSharedInternals.T;
8579-
var currentTransition = {
8580-
_callbacks: new Set()
8581-
};
8579+
var currentTransition = {};
85828580
ReactSharedInternals.T = currentTransition;
85838581

85848582
{
@@ -8591,11 +8589,15 @@ function runActionStateAction(actionQueue, setPendingState, setState, payload) {
85918589

85928590
try {
85938591
var returnValue = action(prevState, payload);
8592+
var onStartTransitionFinish = ReactSharedInternals.S;
8593+
8594+
if (onStartTransitionFinish !== null) {
8595+
onStartTransitionFinish(currentTransition, returnValue);
8596+
}
85948597

85958598
if (returnValue !== null && typeof returnValue === 'object' && // $FlowFixMe[method-unbinding]
85968599
typeof returnValue.then === 'function') {
8597-
var thenable = returnValue;
8598-
notifyTransitionCallbacks(currentTransition, thenable); // Attach a listener to read the return state of the action. As soon as
8600+
var thenable = returnValue; // Attach a listener to read the return state of the action. As soon as
85998601
// this resolves, we can run the next action in the sequence.
86008602

86018603
thenable.then(function (nextState) {
@@ -9100,9 +9102,7 @@ function startTransition(fiber, queue, pendingState, finishedState, callback, op
91009102
var previousPriority = getCurrentUpdatePriority();
91019103
setCurrentUpdatePriority(higherEventPriority(previousPriority, ContinuousEventPriority));
91029104
var prevTransition = ReactSharedInternals.T;
9103-
var currentTransition = {
9104-
_callbacks: new Set()
9105-
};
9105+
var currentTransition = {};
91069106

91079107
{
91089108
// We don't really need to use an optimistic update here, because we
@@ -9121,7 +9121,12 @@ function startTransition(fiber, queue, pendingState, finishedState, callback, op
91219121

91229122
try {
91239123
if (enableAsyncActions) {
9124-
var returnValue = callback(); // Check if we're inside an async action scope. If so, we'll entangle
9124+
var returnValue = callback();
9125+
var onStartTransitionFinish = ReactSharedInternals.S;
9126+
9127+
if (onStartTransitionFinish !== null) {
9128+
onStartTransitionFinish(currentTransition, returnValue);
9129+
} // Check if we're inside an async action scope. If so, we'll entangle
91259130
// this new action with the existing scope.
91269131
//
91279132
// If we're not already inside an async action scope, and this action is
@@ -9130,9 +9135,9 @@ function startTransition(fiber, queue, pendingState, finishedState, callback, op
91309135
// In the async case, the resulting render will suspend until the async
91319136
// action scope has finished.
91329137

9138+
91339139
if (returnValue !== null && typeof returnValue === 'object' && typeof returnValue.then === 'function') {
9134-
var thenable = returnValue;
9135-
notifyTransitionCallbacks(currentTransition, thenable); // Create a thenable that resolves to `finishedState` once the async
9140+
var thenable = returnValue; // Create a thenable that resolves to `finishedState` once the async
91369141
// action has completed.
91379142

91389143
var thenableForFinishedState = chainThenableValue(thenable, finishedState);
@@ -15141,30 +15146,42 @@ function popCacheProvider(workInProgress, cache) {
1514115146
popProvider(CacheContext, workInProgress);
1514215147
}
1514315148

15144-
function requestCurrentTransition() {
15145-
var transition = ReactSharedInternals.T;
15146-
15147-
if (transition !== null) {
15148-
// Whenever a transition update is scheduled, register a callback on the
15149-
// transition object so we can get the return value of the scope function.
15150-
transition._callbacks.add(handleAsyncAction);
15149+
// the shared internals object. This is used by the isomorphic implementation of
15150+
// startTransition to compose all the startTransitions together.
15151+
//
15152+
// function startTransition(fn) {
15153+
// return startTransitionDOM(() => {
15154+
// return startTransitionART(() => {
15155+
// return startTransitionThreeFiber(() => {
15156+
// // and so on...
15157+
// return fn();
15158+
// });
15159+
// });
15160+
// });
15161+
// }
15162+
//
15163+
// Currently we only compose together the code that runs at the end of each
15164+
// startTransition, because for now that's sufficient — the part that sets
15165+
// isTransition=true on the stack uses a separate shared internal field. But
15166+
// really we should delete the shared field and track isTransition per
15167+
// reconciler. Leaving this for a future PR.
15168+
15169+
var prevOnStartTransitionFinish = ReactSharedInternals.S;
15170+
15171+
ReactSharedInternals.S = function onStartTransitionFinishForReconciler(transition, returnValue) {
15172+
if (typeof returnValue === 'object' && returnValue !== null && typeof returnValue.then === 'function') {
15173+
// This is an async action
15174+
var thenable = returnValue;
15175+
entangleAsyncAction(transition, thenable);
1515115176
}
1515215177

15153-
return transition;
15154-
}
15155-
15156-
function handleAsyncAction(transition, thenable) {
15157-
{
15158-
// This is an async action.
15159-
entangleAsyncAction(transition, thenable);
15178+
if (prevOnStartTransitionFinish !== null) {
15179+
prevOnStartTransitionFinish(transition, returnValue);
1516015180
}
15161-
}
15181+
};
1516215182

15163-
function notifyTransitionCallbacks(transition, returnValue) {
15164-
var callbacks = transition._callbacks;
15165-
callbacks.forEach(function (callback) {
15166-
return callback(transition, returnValue);
15167-
});
15183+
function requestCurrentTransition() {
15184+
return ReactSharedInternals.T;
1516815185
} // When retrying a Suspense/Offscreen boundary, we restore the cache that was
1516915186
// used during the previous render by placing it here, on the stack.
1517015187

@@ -23446,7 +23463,7 @@ identifierPrefix, onUncaughtError, onCaughtError, onRecoverableError, transition
2344623463
return root;
2344723464
}
2344823465

23449-
var ReactVersion = '19.0.0-rc-c940c483';
23466+
var ReactVersion = '19.0.0-rc-5a18a922';
2345023467

2345123468
/*
2345223469
* The `'' + value` pattern (used in perf-sensitive code) throws for Symbol

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-test-renderer/cjs/ReactTestRenderer-prod.js

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<8fc32585cc376c14df2f02921b08d02e>>
10+
* @generated SignedSource<<e0bd67bfbe6ced8b6e410eb3974b80d7>>
1111
*/
1212

1313
"use strict";
@@ -2780,16 +2780,18 @@ function runActionStateAction(actionQueue, setPendingState, setState, payload) {
27802780
var action = actionQueue.action,
27812781
prevState = actionQueue.state,
27822782
prevTransition = ReactSharedInternals.T,
2783-
currentTransition = { _callbacks: new Set() };
2783+
currentTransition = {};
27842784
ReactSharedInternals.T = currentTransition;
27852785
setPendingState(!0);
27862786
try {
2787-
var returnValue = action(prevState, payload);
2787+
var returnValue = action(prevState, payload),
2788+
onStartTransitionFinish = ReactSharedInternals.S;
2789+
null !== onStartTransitionFinish &&
2790+
onStartTransitionFinish(currentTransition, returnValue);
27882791
null !== returnValue &&
27892792
"object" === typeof returnValue &&
27902793
"function" === typeof returnValue.then
2791-
? (notifyTransitionCallbacks(currentTransition, returnValue),
2792-
returnValue.then(
2794+
? (returnValue.then(
27932795
function (nextState) {
27942796
actionQueue.state = nextState;
27952797
finishRunningActionStateAction(
@@ -3049,17 +3051,19 @@ function startTransition(fiber, queue, pendingState, finishedState, callback) {
30493051
currentUpdatePriority =
30503052
0 !== previousPriority && 8 > previousPriority ? previousPriority : 8;
30513053
var prevTransition = ReactSharedInternals.T,
3052-
currentTransition = { _callbacks: new Set() };
3054+
currentTransition = {};
30533055
ReactSharedInternals.T = currentTransition;
30543056
dispatchOptimisticSetState(fiber, !1, queue, pendingState);
30553057
try {
3056-
var returnValue = callback();
3058+
var returnValue = callback(),
3059+
onStartTransitionFinish = ReactSharedInternals.S;
3060+
null !== onStartTransitionFinish &&
3061+
onStartTransitionFinish(currentTransition, returnValue);
30573062
if (
30583063
null !== returnValue &&
30593064
"object" === typeof returnValue &&
30603065
"function" === typeof returnValue.then
30613066
) {
3062-
notifyTransitionCallbacks(currentTransition, returnValue);
30633067
var thenableForFinishedState = chainThenableValue(
30643068
returnValue,
30653069
finishedState
@@ -3160,7 +3164,6 @@ function dispatchSetState(fiber, queue, action) {
31603164
}
31613165
}
31623166
function dispatchOptimisticSetState(fiber, throwIfDuringRender, queue, action) {
3163-
requestCurrentTransition();
31643167
action = {
31653168
lane: 2,
31663169
revertLane: requestTransitionLane(),
@@ -5602,19 +5605,15 @@ function releaseCache(cache) {
56025605
cache.controller.abort();
56035606
});
56045607
}
5605-
function requestCurrentTransition() {
5606-
var transition = ReactSharedInternals.T;
5607-
null !== transition && transition._callbacks.add(handleAsyncAction);
5608-
return transition;
5609-
}
5610-
function handleAsyncAction(transition, thenable) {
5611-
entangleAsyncAction(transition, thenable);
5612-
}
5613-
function notifyTransitionCallbacks(transition, returnValue) {
5614-
transition._callbacks.forEach(function (callback) {
5615-
return callback(transition, returnValue);
5616-
});
5617-
}
5608+
var prevOnStartTransitionFinish = ReactSharedInternals.S;
5609+
ReactSharedInternals.S = function (transition, returnValue) {
5610+
"object" === typeof returnValue &&
5611+
null !== returnValue &&
5612+
"function" === typeof returnValue.then &&
5613+
entangleAsyncAction(transition, returnValue);
5614+
null !== prevOnStartTransitionFinish &&
5615+
prevOnStartTransitionFinish(transition, returnValue);
5616+
};
56185617
var resumedCache = createCursor(null);
56195618
function peekCacheFromPool() {
56205619
var cacheResumedFromPreviousRender = resumedCache.current;
@@ -7573,7 +7572,7 @@ function requestUpdateLane(fiber) {
75737572
if (0 === (fiber.mode & 1)) return 2;
75747573
if (0 !== (executionContext & 2) && 0 !== workInProgressRootRenderLanes)
75757574
return workInProgressRootRenderLanes & -workInProgressRootRenderLanes;
7576-
if (null !== requestCurrentTransition())
7575+
if (null !== ReactSharedInternals.T)
75777576
return (
75787577
(fiber = currentEntangledLane),
75797578
0 !== fiber ? fiber : requestTransitionLane()
@@ -9301,7 +9300,7 @@ var devToolsConfig$jscomp$inline_1042 = {
93019300
throw Error("TestRenderer does not support findFiberByHostInstance()");
93029301
},
93039302
bundleType: 0,
9304-
version: "19.0.0-rc-2bec340c",
9303+
version: "19.0.0-rc-9c2862e7",
93059304
rendererPackageName: "react-test-renderer"
93069305
};
93079306
var internals$jscomp$inline_1229 = {
@@ -9332,7 +9331,7 @@ var internals$jscomp$inline_1229 = {
93329331
scheduleRoot: null,
93339332
setRefreshHandler: null,
93349333
getCurrentFiber: null,
9335-
reconcilerVersion: "19.0.0-rc-2bec340c"
9334+
reconcilerVersion: "19.0.0-rc-9c2862e7"
93369335
};
93379336
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
93389337
var hook$jscomp$inline_1230 = __REACT_DEVTOOLS_GLOBAL_HOOK__;

compiled-rn/facebook-fbsource/xplat/js/RKJSModules/vendor/react/react-test-renderer/cjs/ReactTestRenderer-profiling.js

Lines changed: 24 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<f60fad93c112dcc72285bcbbda070adc>>
10+
* @generated SignedSource<<cd5cd2d51aad2dcc6d485f9a9bc90b90>>
1111
*/
1212

1313
"use strict";
@@ -2868,16 +2868,18 @@ function runActionStateAction(actionQueue, setPendingState, setState, payload) {
28682868
var action = actionQueue.action,
28692869
prevState = actionQueue.state,
28702870
prevTransition = ReactSharedInternals.T,
2871-
currentTransition = { _callbacks: new Set() };
2871+
currentTransition = {};
28722872
ReactSharedInternals.T = currentTransition;
28732873
setPendingState(!0);
28742874
try {
2875-
var returnValue = action(prevState, payload);
2875+
var returnValue = action(prevState, payload),
2876+
onStartTransitionFinish = ReactSharedInternals.S;
2877+
null !== onStartTransitionFinish &&
2878+
onStartTransitionFinish(currentTransition, returnValue);
28762879
null !== returnValue &&
28772880
"object" === typeof returnValue &&
28782881
"function" === typeof returnValue.then
2879-
? (notifyTransitionCallbacks(currentTransition, returnValue),
2880-
returnValue.then(
2882+
? (returnValue.then(
28812883
function (nextState) {
28822884
actionQueue.state = nextState;
28832885
finishRunningActionStateAction(
@@ -3139,17 +3141,19 @@ function startTransition(fiber, queue, pendingState, finishedState, callback) {
31393141
currentUpdatePriority =
31403142
0 !== previousPriority && 8 > previousPriority ? previousPriority : 8;
31413143
var prevTransition = ReactSharedInternals.T,
3142-
currentTransition = { _callbacks: new Set() };
3144+
currentTransition = {};
31433145
ReactSharedInternals.T = currentTransition;
31443146
dispatchOptimisticSetState(fiber, !1, queue, pendingState);
31453147
try {
3146-
var returnValue = callback();
3148+
var returnValue = callback(),
3149+
onStartTransitionFinish = ReactSharedInternals.S;
3150+
null !== onStartTransitionFinish &&
3151+
onStartTransitionFinish(currentTransition, returnValue);
31473152
if (
31483153
null !== returnValue &&
31493154
"object" === typeof returnValue &&
31503155
"function" === typeof returnValue.then
31513156
) {
3152-
notifyTransitionCallbacks(currentTransition, returnValue);
31533157
var thenableForFinishedState = chainThenableValue(
31543158
returnValue,
31553159
finishedState
@@ -3252,7 +3256,6 @@ function dispatchSetState(fiber, queue, action) {
32523256
markStateUpdateScheduled(fiber, lane);
32533257
}
32543258
function dispatchOptimisticSetState(fiber, throwIfDuringRender, queue, action) {
3255-
requestCurrentTransition();
32563259
action = {
32573260
lane: 2,
32583261
revertLane: requestTransitionLane(),
@@ -5809,19 +5812,15 @@ function releaseCache(cache) {
58095812
cache.controller.abort();
58105813
});
58115814
}
5812-
function requestCurrentTransition() {
5813-
var transition = ReactSharedInternals.T;
5814-
null !== transition && transition._callbacks.add(handleAsyncAction);
5815-
return transition;
5816-
}
5817-
function handleAsyncAction(transition, thenable) {
5818-
entangleAsyncAction(transition, thenable);
5819-
}
5820-
function notifyTransitionCallbacks(transition, returnValue) {
5821-
transition._callbacks.forEach(function (callback) {
5822-
return callback(transition, returnValue);
5823-
});
5824-
}
5815+
var prevOnStartTransitionFinish = ReactSharedInternals.S;
5816+
ReactSharedInternals.S = function (transition, returnValue) {
5817+
"object" === typeof returnValue &&
5818+
null !== returnValue &&
5819+
"function" === typeof returnValue.then &&
5820+
entangleAsyncAction(transition, returnValue);
5821+
null !== prevOnStartTransitionFinish &&
5822+
prevOnStartTransitionFinish(transition, returnValue);
5823+
};
58255824
var resumedCache = createCursor(null);
58265825
function peekCacheFromPool() {
58275826
var cacheResumedFromPreviousRender = resumedCache.current;
@@ -8056,7 +8055,7 @@ function requestUpdateLane(fiber) {
80568055
if (0 === (fiber.mode & 1)) return 2;
80578056
if (0 !== (executionContext & 2) && 0 !== workInProgressRootRenderLanes)
80588057
return workInProgressRootRenderLanes & -workInProgressRootRenderLanes;
8059-
if (null !== requestCurrentTransition())
8058+
if (null !== ReactSharedInternals.T)
80608059
return (
80618060
(fiber = currentEntangledLane),
80628061
0 !== fiber ? fiber : requestTransitionLane()
@@ -9944,7 +9943,7 @@ var devToolsConfig$jscomp$inline_1105 = {
99449943
throw Error("TestRenderer does not support findFiberByHostInstance()");
99459944
},
99469945
bundleType: 0,
9947-
version: "19.0.0-rc-e8e0cdf6",
9946+
version: "19.0.0-rc-6cf53e9c",
99489947
rendererPackageName: "react-test-renderer"
99499948
};
99509949
(function (internals) {
@@ -9988,7 +9987,7 @@ var devToolsConfig$jscomp$inline_1105 = {
99889987
scheduleRoot: null,
99899988
setRefreshHandler: null,
99909989
getCurrentFiber: null,
9991-
reconcilerVersion: "19.0.0-rc-e8e0cdf6"
9990+
reconcilerVersion: "19.0.0-rc-6cf53e9c"
99929991
});
99939992
exports._Scheduler = Scheduler;
99949993
exports.act = act;

0 commit comments

Comments
 (0)