Skip to content

Commit cf74ec9

Browse files
committed
Async action support for React.startTransition (#28097)
This adds support for async actions to the "isomorphic" version of startTransition (i.e. the one exported by the "react" package). Previously, async actions were only supported by the startTransition that is returned from the useTransition hook. The interesting part about the isomorphic startTransition is that it's not associated with any particular root. It must work with updates to arbitrary roots, or even arbitrary React renderers in the same app. (For example, both React DOM and React Three Fiber.) The idea is that React.startTransition should behave as if every root had an implicit useTransition hook, and you composed together all the startTransitions provided by those hooks. Multiple updates to the same root will be batched together. However, updates to one root will not be batched with updates to other roots. Features like useOptimistic work the same as with the hook version. There is one difference from from the hook version of startTransition: an error triggered inside an async action cannot be captured by an error boundary, because it's not associated with any particular part of the tree. You should handle errors the same way you would in a regular event, e.g. with a global error event handler, or with a local `try/catch`. DiffTrain build for commit 85b296e.
1 parent 5fbb6a2 commit cf74ec9

File tree

13 files changed

+728
-691
lines changed

13 files changed

+728
-691
lines changed

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

Lines changed: 73 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
* @noflow
88
* @nolint
99
* @preventMunge
10-
* @generated SignedSource<<6f91cae5b10795257ddec042012ed6d4>>
10+
* @generated SignedSource<<44707665e2d49df89524e6821112ab6c>>
1111
*/
1212

1313
"use strict";
@@ -3469,7 +3469,11 @@ if (__DEV__) {
34693469
}
34703470
}
34713471

3472-
function requestTransitionLane() {
3472+
function requestTransitionLane( // This argument isn't used, it's only here to encourage the caller to
3473+
// check that it's inside a transition before calling this function.
3474+
// TODO: Make this non-nullable. Requires a tweak to useOptimistic.
3475+
transition
3476+
) {
34733477
// The algorithm for assigning an update to a lane should be stable for all
34743478
// updates at the same priority within the same event. To do this, the
34753479
// inputs to the algorithm must be the same.
@@ -3502,7 +3506,7 @@ if (__DEV__) {
35023506
// until the async action scope has completed.
35033507

35043508
var currentEntangledActionThenable = null;
3505-
function entangleAsyncAction(thenable) {
3509+
function entangleAsyncAction(transition, thenable) {
35063510
// `thenable` is the return value of the async action scope function. Create
35073511
// a combined thenable that resolves once every entangled scope function
35083512
// has finished.
@@ -7738,16 +7742,7 @@ if (__DEV__) {
77387742
markSkippedUpdateLanes(updateLane);
77397743
} else {
77407744
// This update does have sufficient priority.
7741-
// Check if this update is part of a pending async action. If so,
7742-
// we'll need to suspend until the action has finished, so that it's
7743-
// batched together with future updates in the same action.
7744-
if (
7745-
updateLane !== NoLane &&
7746-
updateLane === peekEntangledActionLane()
7747-
) {
7748-
didReadFromEntangledAsyncAction = true;
7749-
} // Check if this is an optimistic update.
7750-
7745+
// Check if this is an optimistic update.
77517746
var revertLane = update.revertLane;
77527747

77537748
if (revertLane === NoLane) {
@@ -7767,6 +7762,12 @@ if (__DEV__) {
77677762
next: null
77687763
};
77697764
newBaseQueueLast = newBaseQueueLast.next = _clone;
7765+
} // Check if this update is part of a pending async action. If so,
7766+
// we'll need to suspend until the action has finished, so that it's
7767+
// batched together with future updates in the same action.
7768+
7769+
if (updateLane === peekEntangledActionLane()) {
7770+
didReadFromEntangledAsyncAction = true;
77707771
}
77717772
} else {
77727773
// This is an optimistic update. If the "revert" priority is
@@ -7777,7 +7778,14 @@ if (__DEV__) {
77777778
// The transition that this optimistic update is associated with
77787779
// has finished. Pretend the update doesn't exist by skipping
77797780
// over it.
7780-
update = update.next;
7781+
update = update.next; // Check if this update is part of a pending async action. If so,
7782+
// we'll need to suspend until the action has finished, so that it's
7783+
// batched together with future updates in the same action.
7784+
7785+
if (revertLane === peekEntangledActionLane()) {
7786+
didReadFromEntangledAsyncAction = true;
7787+
}
7788+
77817789
continue;
77827790
} else {
77837791
var _clone2 = {
@@ -8288,15 +8296,18 @@ if (__DEV__) {
82888296
var prevState = actionQueue.state; // This is a fork of startTransition
82898297

82908298
var prevTransition = ReactCurrentBatchConfig$2.transition;
8291-
ReactCurrentBatchConfig$2.transition = {};
8292-
var currentTransition = ReactCurrentBatchConfig$2.transition;
8299+
var currentTransition = {
8300+
_callbacks: new Set()
8301+
};
8302+
ReactCurrentBatchConfig$2.transition = currentTransition;
82938303

82948304
{
82958305
ReactCurrentBatchConfig$2.transition._updatedFibers = new Set();
82968306
}
82978307

82988308
try {
82998309
var returnValue = action(prevState, payload);
8310+
notifyTransitionCallbacks(currentTransition, returnValue);
83008311

83018312
if (
83028313
returnValue !== null &&
@@ -8315,7 +8326,6 @@ if (__DEV__) {
83158326
return finishRunningFormStateAction(actionQueue, setState);
83168327
}
83178328
);
8318-
entangleAsyncAction(thenable);
83198329
setState(thenable);
83208330
} else {
83218331
setState(returnValue);
@@ -8876,7 +8886,9 @@ if (__DEV__) {
88768886
higherEventPriority(previousPriority, ContinuousEventPriority)
88778887
);
88788888
var prevTransition = ReactCurrentBatchConfig$2.transition;
8879-
var currentTransition = {};
8889+
var currentTransition = {
8890+
_callbacks: new Set()
8891+
};
88808892

88818893
{
88828894
// We don't really need to use an optimistic update here, because we
@@ -8895,7 +8907,8 @@ if (__DEV__) {
88958907

88968908
try {
88978909
if (enableAsyncActions) {
8898-
var returnValue = callback(); // Check if we're inside an async action scope. If so, we'll entangle
8910+
var returnValue = callback();
8911+
notifyTransitionCallbacks(currentTransition, returnValue); // Check if we're inside an async action scope. If so, we'll entangle
88998912
// this new action with the existing scope.
89008913
//
89018914
// If we're not already inside an async action scope, and this action is
@@ -8909,8 +8922,7 @@ if (__DEV__) {
89098922
typeof returnValue === "object" &&
89108923
typeof returnValue.then === "function"
89118924
) {
8912-
var thenable = returnValue;
8913-
entangleAsyncAction(thenable); // Create a thenable that resolves to `finishedState` once the async
8925+
var thenable = returnValue; // Create a thenable that resolves to `finishedState` once the async
89148926
// action has completed.
89158927

89168928
var thenableForFinishedState = chainThenableValue(
@@ -9214,8 +9226,10 @@ if (__DEV__) {
92149226
queue,
92159227
action
92169228
) {
9229+
var transition = requestCurrentTransition();
9230+
92179231
{
9218-
if (ReactCurrentBatchConfig$2.transition === null) {
9232+
if (transition === null) {
92199233
// An optimistic update occurred, but startTransition is not on the stack.
92209234
// There are two likely scenarios.
92219235
// One possibility is that the optimistic update is triggered by a regular
@@ -16268,9 +16282,35 @@ if (__DEV__) {
1626816282

1626916283
var ReactCurrentBatchConfig$1 =
1627016284
ReactSharedInternals.ReactCurrentBatchConfig;
16271-
var NoTransition = null;
1627216285
function requestCurrentTransition() {
16273-
return ReactCurrentBatchConfig$1.transition;
16286+
var transition = ReactCurrentBatchConfig$1.transition;
16287+
16288+
if (transition !== null) {
16289+
// Whenever a transition update is scheduled, register a callback on the
16290+
// transition object so we can get the return value of the scope function.
16291+
transition._callbacks.add(handleTransitionScopeResult);
16292+
}
16293+
16294+
return transition;
16295+
}
16296+
16297+
function handleTransitionScopeResult(transition, returnValue) {
16298+
if (
16299+
returnValue !== null &&
16300+
typeof returnValue === "object" &&
16301+
typeof returnValue.then === "function"
16302+
) {
16303+
// This is an async action.
16304+
var thenable = returnValue;
16305+
entangleAsyncAction(transition, thenable);
16306+
}
16307+
}
16308+
16309+
function notifyTransitionCallbacks(transition, returnValue) {
16310+
var callbacks = transition._callbacks;
16311+
callbacks.forEach(function (callback) {
16312+
return callback(transition, returnValue);
16313+
});
1627416314
} // When retrying a Suspense/Offscreen boundary, we restore the cache that was
1627516315
// used during the previous render by placing it here, on the stack.
1627616316

@@ -21566,17 +21606,17 @@ if (__DEV__) {
2156621606
return pickArbitraryLane(workInProgressRootRenderLanes);
2156721607
}
2156821608

21569-
var isTransition = requestCurrentTransition() !== NoTransition;
21609+
var transition = requestCurrentTransition();
2157021610

21571-
if (isTransition) {
21572-
if (ReactCurrentBatchConfig.transition !== null) {
21573-
var transition = ReactCurrentBatchConfig.transition;
21611+
if (transition !== null) {
21612+
{
21613+
var batchConfigTransition = ReactCurrentBatchConfig.transition;
2157421614

21575-
if (!transition._updatedFibers) {
21576-
transition._updatedFibers = new Set();
21615+
if (!batchConfigTransition._updatedFibers) {
21616+
batchConfigTransition._updatedFibers = new Set();
2157721617
}
2157821618

21579-
transition._updatedFibers.add(fiber);
21619+
batchConfigTransition._updatedFibers.add(fiber);
2158021620
}
2158121621

2158221622
var actionScopeLane = peekEntangledActionLane();
@@ -21643,7 +21683,7 @@ if (__DEV__) {
2164321683
workInProgressDeferredLane = OffscreenLane;
2164421684
} else {
2164521685
// Everything else is spawned as a transition.
21646-
workInProgressDeferredLane = requestTransitionLane();
21686+
workInProgressDeferredLane = claimNextTransitionLane();
2164721687
}
2164821688
} // Mark the parent Suspense boundary so it knows to spawn the deferred lane.
2164921689

@@ -25572,7 +25612,7 @@ if (__DEV__) {
2557225612
return root;
2557325613
}
2557425614

25575-
var ReactVersion = "18.3.0-canary-382190c59-20240125";
25615+
var ReactVersion = "18.3.0-canary-85b296e9b-20240125";
2557625616

2557725617
// Might add PROFILE later.
2557825618

0 commit comments

Comments
 (0)