Skip to content

Commit a56f5cf

Browse files
committed
useActionState: Transfer transition context (#29694)
Mini-refactor of useActionState to only wrap the action in a transition context if the dispatch is called during a transition. Conceptually, the action starts as soon as the dispatch is called, even if the action is queued until earlier ones finish. We will also warn if an async action is dispatched outside of a transition, since that is almost certainly a mistake. Ideally we would automatically upgrade these to a transition, but we don't have a great way to tell if the action is async until after it's already run. DiffTrain build for [67b05be](67b05be)
1 parent 6c317aa commit a56f5cf

35 files changed

+4247
-3242
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
def67b9b329c8aa204e611cd510c5a64680aee58
1+
67b05be0d216c4efebc4bb5acb12c861a18bd87c
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
def67b9b329c8aa204e611cd510c5a64680aee58
1+
67b05be0d216c4efebc4bb5acb12c861a18bd87c

compiled/facebook-www/React-dev.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ if (
2222
) {
2323
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2424
}
25-
var ReactVersion = '19.0.0-www-classic-def67b9b32-20240603';
25+
var ReactVersion = '19.0.0-www-classic-67b05be0d2-20240603';
2626

2727
// Re-export dynamic flags from the www version.
2828
var dynamicFeatureFlags = require('ReactFeatureFlags');

compiled/facebook-www/React-dev.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ if (
2222
) {
2323
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
2424
}
25-
var ReactVersion = '19.0.0-www-modern-def67b9b32-20240603';
25+
var ReactVersion = '19.0.0-www-modern-67b05be0d2-20240603';
2626

2727
// Re-export dynamic flags from the www version.
2828
var dynamicFeatureFlags = require('ReactFeatureFlags');

compiled/facebook-www/React-prod.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,4 +684,4 @@ exports.useSyncExternalStore = function (
684684
exports.useTransition = function () {
685685
return ReactSharedInternals.H.useTransition();
686686
};
687-
exports.version = "19.0.0-www-classic-def67b9b32-20240603";
687+
exports.version = "19.0.0-www-classic-67b05be0d2-20240603";

compiled/facebook-www/React-prod.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -684,4 +684,4 @@ exports.useSyncExternalStore = function (
684684
exports.useTransition = function () {
685685
return ReactSharedInternals.H.useTransition();
686686
};
687-
exports.version = "19.0.0-www-modern-def67b9b32-20240603";
687+
exports.version = "19.0.0-www-modern-67b05be0d2-20240603";

compiled/facebook-www/React-profiling.classic.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ exports.useSyncExternalStore = function (
688688
exports.useTransition = function () {
689689
return ReactSharedInternals.H.useTransition();
690690
};
691-
exports.version = "19.0.0-www-classic-def67b9b32-20240603";
691+
exports.version = "19.0.0-www-classic-67b05be0d2-20240603";
692692
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
693693
"function" ===
694694
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/React-profiling.modern.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -688,7 +688,7 @@ exports.useSyncExternalStore = function (
688688
exports.useTransition = function () {
689689
return ReactSharedInternals.H.useTransition();
690690
};
691-
exports.version = "19.0.0-www-modern-def67b9b32-20240603";
691+
exports.version = "19.0.0-www-modern-67b05be0d2-20240603";
692692
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
693693
"function" ===
694694
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&

compiled/facebook-www/ReactART-dev.classic.js

Lines changed: 147 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ function _assertThisInitialized(self) {
6060
return self;
6161
}
6262

63-
var ReactVersion = '19.0.0-www-classic-def67b9b32-20240603';
63+
var ReactVersion = '19.0.0-www-classic-67b05be0d2-20240603';
6464

6565
var LegacyRoot = 0;
6666
var ConcurrentRoot = 1;
@@ -9272,112 +9272,148 @@ function dispatchActionState(fiber, actionQueue, setPendingState, setState, payl
92729272
throw new Error('Cannot update form state while rendering.');
92739273
}
92749274

9275+
var actionNode = {
9276+
payload: payload,
9277+
action: actionQueue.action,
9278+
next: null,
9279+
// circular
9280+
isTransition: true,
9281+
status: 'pending',
9282+
value: null,
9283+
reason: null,
9284+
listeners: [],
9285+
then: function (listener) {
9286+
// We know the only thing that subscribes to these promises is `use` so
9287+
// this implementation is simpler than a generic thenable. E.g. we don't
9288+
// bother to check if the thenable is still pending because `use` already
9289+
// does that.
9290+
actionNode.listeners.push(listener);
9291+
}
9292+
}; // Check if we're inside a transition. If so, we'll need to restore the
9293+
// transition context when the action is run.
9294+
9295+
var prevTransition = ReactSharedInternals.T;
9296+
9297+
if (prevTransition !== null) {
9298+
// Optimistically update the pending state, similar to useTransition.
9299+
// This will be reverted automatically when all actions are finished.
9300+
setPendingState(true); // `actionNode` is a thenable that resolves to the return value of
9301+
// the action.
9302+
9303+
setState(actionNode);
9304+
} else {
9305+
// This is not a transition.
9306+
actionNode.isTransition = false;
9307+
setState(actionNode);
9308+
}
9309+
92759310
var last = actionQueue.pending;
92769311

92779312
if (last === null) {
92789313
// There are no pending actions; this is the first one. We can run
92799314
// it immediately.
9280-
var newLast = {
9281-
payload: payload,
9282-
action: actionQueue.action,
9283-
next: null // circular
9284-
9285-
};
9286-
newLast.next = actionQueue.pending = newLast;
9287-
runActionStateAction(actionQueue, setPendingState, setState, newLast);
9315+
actionNode.next = actionQueue.pending = actionNode;
9316+
runActionStateAction(actionQueue, actionNode);
92889317
} else {
92899318
// There's already an action running. Add to the queue.
92909319
var first = last.next;
9291-
var _newLast = {
9292-
payload: payload,
9293-
action: actionQueue.action,
9294-
next: first
9295-
};
9296-
actionQueue.pending = last.next = _newLast;
9320+
actionNode.next = first;
9321+
actionQueue.pending = last.next = actionNode;
92979322
}
92989323
}
92999324

9300-
function runActionStateAction(actionQueue, setPendingState, setState, node) {
9301-
// This is a fork of startTransition
9302-
var prevTransition = ReactSharedInternals.T;
9303-
var currentTransition = {};
9304-
ReactSharedInternals.T = currentTransition;
9305-
9306-
{
9307-
ReactSharedInternals.T._updatedFibers = new Set();
9308-
} // Optimistically update the pending state, similar to useTransition.
9309-
// This will be reverted automatically when all actions are finished.
9310-
9311-
9312-
setPendingState(true); // `node.action` represents the action function at the time it was dispatched.
9325+
function runActionStateAction(actionQueue, node) {
9326+
// `node.action` represents the action function at the time it was dispatched.
93139327
// If this action was queued, it might be stale, i.e. it's not necessarily the
93149328
// most current implementation of the action, stored on `actionQueue`. This is
93159329
// intentional. The conceptual model for queued actions is that they are
93169330
// queued in a remote worker; the dispatch happens immediately, only the
93179331
// execution is delayed.
9318-
93199332
var action = node.action;
93209333
var payload = node.payload;
93219334
var prevState = actionQueue.state;
93229335

9323-
try {
9324-
var returnValue = action(prevState, payload);
9325-
var onStartTransitionFinish = ReactSharedInternals.S;
9336+
if (node.isTransition) {
9337+
// The original dispatch was part of a transition. We restore its
9338+
// transition context here.
9339+
// This is a fork of startTransition
9340+
var prevTransition = ReactSharedInternals.T;
9341+
var currentTransition = {};
9342+
ReactSharedInternals.T = currentTransition;
93269343

9327-
if (onStartTransitionFinish !== null) {
9328-
onStartTransitionFinish(currentTransition, returnValue);
9344+
{
9345+
ReactSharedInternals.T._updatedFibers = new Set();
93299346
}
93309347

9331-
if (returnValue !== null && typeof returnValue === 'object' && // $FlowFixMe[method-unbinding]
9332-
typeof returnValue.then === 'function') {
9333-
var thenable = returnValue; // Attach a listener to read the return state of the action. As soon as
9334-
// this resolves, we can run the next action in the sequence.
9348+
try {
9349+
var returnValue = action(prevState, payload);
9350+
var onStartTransitionFinish = ReactSharedInternals.S;
93359351

9336-
thenable.then(function (nextState) {
9337-
actionQueue.state = nextState;
9338-
finishRunningActionStateAction(actionQueue, setPendingState, setState);
9339-
}, function () {
9340-
return finishRunningActionStateAction(actionQueue, setPendingState, setState);
9341-
});
9342-
setState(thenable);
9343-
} else {
9344-
setState(returnValue);
9345-
var nextState = returnValue;
9346-
actionQueue.state = nextState;
9347-
finishRunningActionStateAction(actionQueue, setPendingState, setState);
9348-
}
9349-
} catch (error) {
9350-
// This is a trick to get the `useActionState` hook to rethrow the error.
9351-
// When it unwraps the thenable with the `use` algorithm, the error
9352-
// will be thrown.
9353-
var rejectedThenable = {
9354-
then: function () {},
9355-
status: 'rejected',
9356-
reason: error // $FlowFixMe: Not sure why this doesn't work
9352+
if (onStartTransitionFinish !== null) {
9353+
onStartTransitionFinish(currentTransition, returnValue);
9354+
}
93579355

9358-
};
9359-
setState(rejectedThenable);
9360-
finishRunningActionStateAction(actionQueue, setPendingState, setState);
9361-
} finally {
9362-
ReactSharedInternals.T = prevTransition;
9356+
handleActionReturnValue(actionQueue, node, returnValue);
9357+
} catch (error) {
9358+
onActionError(actionQueue, node, error);
9359+
} finally {
9360+
ReactSharedInternals.T = prevTransition;
93639361

9364-
{
9365-
if (prevTransition === null && currentTransition._updatedFibers) {
9366-
var updatedFibersCount = currentTransition._updatedFibers.size;
9362+
{
9363+
if (prevTransition === null && currentTransition._updatedFibers) {
9364+
var updatedFibersCount = currentTransition._updatedFibers.size;
93679365

9368-
currentTransition._updatedFibers.clear();
9366+
currentTransition._updatedFibers.clear();
93699367

9370-
if (updatedFibersCount > 10) {
9371-
warn('Detected a large number of updates inside startTransition. ' + 'If this is due to a subscription please re-write it to use React provided hooks. ' + 'Otherwise concurrent mode guarantees are off the table.');
9368+
if (updatedFibersCount > 10) {
9369+
warn('Detected a large number of updates inside startTransition. ' + 'If this is due to a subscription please re-write it to use React provided hooks. ' + 'Otherwise concurrent mode guarantees are off the table.');
9370+
}
93729371
}
93739372
}
93749373
}
9374+
} else {
9375+
// The original dispatch was not part of a transition.
9376+
try {
9377+
var _returnValue = action(prevState, payload);
9378+
9379+
handleActionReturnValue(actionQueue, node, _returnValue);
9380+
} catch (error) {
9381+
onActionError(actionQueue, node, error);
9382+
}
9383+
}
9384+
}
9385+
9386+
function handleActionReturnValue(actionQueue, node, returnValue) {
9387+
if (returnValue !== null && typeof returnValue === 'object' && // $FlowFixMe[method-unbinding]
9388+
typeof returnValue.then === 'function') {
9389+
var thenable = returnValue; // Attach a listener to read the return state of the action. As soon as
9390+
// this resolves, we can run the next action in the sequence.
9391+
9392+
thenable.then(function (nextState) {
9393+
onActionSuccess(actionQueue, node, nextState);
9394+
}, function (error) {
9395+
return onActionError(actionQueue, node, error);
9396+
});
9397+
9398+
{
9399+
if (!node.isTransition) {
9400+
error('An async function was passed to useActionState, but it was ' + 'dispatched outside of an action context. This is likely not ' + 'what you intended. Either pass the dispatch function to an ' + '`action` prop, or dispatch manually inside `startTransition`');
9401+
}
9402+
}
9403+
} else {
9404+
var nextState = returnValue;
9405+
onActionSuccess(actionQueue, node, nextState);
93759406
}
93769407
}
93779408

9378-
function finishRunningActionStateAction(actionQueue, setPendingState, setState) {
9379-
// The action finished running. Pop it from the queue and run the next pending
9380-
// action, if there are any.
9409+
function onActionSuccess(actionQueue, actionNode, nextState) {
9410+
// The action finished running.
9411+
actionNode.status = 'fulfilled';
9412+
actionNode.value = nextState;
9413+
notifyActionListeners(actionNode);
9414+
actionQueue.state = nextState; // Pop the action from the queue and run the next pending action, if there
9415+
// are any.
9416+
93819417
var last = actionQueue.pending;
93829418

93839419
if (last !== null) {
@@ -9391,11 +9427,48 @@ function finishRunningActionStateAction(actionQueue, setPendingState, setState)
93919427
var next = first.next;
93929428
last.next = next; // Run the next action.
93939429

9394-
runActionStateAction(actionQueue, setPendingState, setState, next);
9430+
runActionStateAction(actionQueue, next);
93959431
}
93969432
}
93979433
}
93989434

9435+
function onActionError(actionQueue, actionNode, error) {
9436+
actionNode.status = 'rejected';
9437+
actionNode.reason = error;
9438+
notifyActionListeners(actionNode); // Pop the action from the queue and run the next pending action, if there
9439+
// are any.
9440+
// TODO: We should instead abort all the remaining actions in the queue.
9441+
9442+
var last = actionQueue.pending;
9443+
9444+
if (last !== null) {
9445+
var first = last.next;
9446+
9447+
if (first === last) {
9448+
// This was the last action in the queue.
9449+
actionQueue.pending = null;
9450+
} else {
9451+
// Remove the first node from the circular queue.
9452+
var next = first.next;
9453+
last.next = next; // Run the next action.
9454+
9455+
runActionStateAction(actionQueue, next);
9456+
}
9457+
}
9458+
}
9459+
9460+
function notifyActionListeners(actionNode) {
9461+
// Notify React that the action has finished.
9462+
var listeners = actionNode.listeners;
9463+
9464+
for (var i = 0; i < listeners.length; i++) {
9465+
// This is always a React internal listener, so we don't need to worry
9466+
// about it throwing.
9467+
var listener = listeners[i];
9468+
listener();
9469+
}
9470+
}
9471+
93999472
function actionStateReducer(oldState, newState) {
94009473
return newState;
94019474
}

0 commit comments

Comments
 (0)