Skip to content

Commit

Permalink
useActionState: Transfer transition context (#29694)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
acdlite committed Jun 3, 2024
1 parent 6c317aa commit a56f5cf
Show file tree
Hide file tree
Showing 35 changed files with 4,247 additions and 3,242 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
def67b9b329c8aa204e611cd510c5a64680aee58
67b05be0d216c4efebc4bb5acb12c861a18bd87c
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION_TRANSFORMS
Original file line number Diff line number Diff line change
@@ -1 +1 @@
def67b9b329c8aa204e611cd510c5a64680aee58
67b05be0d216c4efebc4bb5acb12c861a18bd87c
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ if (
) {
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
}
var ReactVersion = '19.0.0-www-classic-def67b9b32-20240603';
var ReactVersion = '19.0.0-www-classic-67b05be0d2-20240603';

// Re-export dynamic flags from the www version.
var dynamicFeatureFlags = require('ReactFeatureFlags');
Expand Down
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ if (
) {
__REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStart(new Error());
}
var ReactVersion = '19.0.0-www-modern-def67b9b32-20240603';
var ReactVersion = '19.0.0-www-modern-67b05be0d2-20240603';

// Re-export dynamic flags from the www version.
var dynamicFeatureFlags = require('ReactFeatureFlags');
Expand Down
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-prod.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -684,4 +684,4 @@ exports.useSyncExternalStore = function (
exports.useTransition = function () {
return ReactSharedInternals.H.useTransition();
};
exports.version = "19.0.0-www-classic-def67b9b32-20240603";
exports.version = "19.0.0-www-classic-67b05be0d2-20240603";
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -684,4 +684,4 @@ exports.useSyncExternalStore = function (
exports.useTransition = function () {
return ReactSharedInternals.H.useTransition();
};
exports.version = "19.0.0-www-modern-def67b9b32-20240603";
exports.version = "19.0.0-www-modern-67b05be0d2-20240603";
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-profiling.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ exports.useSyncExternalStore = function (
exports.useTransition = function () {
return ReactSharedInternals.H.useTransition();
};
exports.version = "19.0.0-www-classic-def67b9b32-20240603";
exports.version = "19.0.0-www-classic-67b05be0d2-20240603";
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
"function" ===
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&
Expand Down
2 changes: 1 addition & 1 deletion compiled/facebook-www/React-profiling.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -688,7 +688,7 @@ exports.useSyncExternalStore = function (
exports.useTransition = function () {
return ReactSharedInternals.H.useTransition();
};
exports.version = "19.0.0-www-modern-def67b9b32-20240603";
exports.version = "19.0.0-www-modern-67b05be0d2-20240603";
"undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__ &&
"function" ===
typeof __REACT_DEVTOOLS_GLOBAL_HOOK__.registerInternalModuleStop &&
Expand Down
221 changes: 147 additions & 74 deletions compiled/facebook-www/ReactART-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ function _assertThisInitialized(self) {
return self;
}

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

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

var actionNode = {
payload: payload,
action: actionQueue.action,
next: null,
// circular
isTransition: true,
status: 'pending',
value: null,
reason: null,
listeners: [],
then: function (listener) {
// We know the only thing that subscribes to these promises is `use` so
// this implementation is simpler than a generic thenable. E.g. we don't
// bother to check if the thenable is still pending because `use` already
// does that.
actionNode.listeners.push(listener);
}
}; // Check if we're inside a transition. If so, we'll need to restore the
// transition context when the action is run.

var prevTransition = ReactSharedInternals.T;

if (prevTransition !== null) {
// Optimistically update the pending state, similar to useTransition.
// This will be reverted automatically when all actions are finished.
setPendingState(true); // `actionNode` is a thenable that resolves to the return value of
// the action.

setState(actionNode);
} else {
// This is not a transition.
actionNode.isTransition = false;
setState(actionNode);
}

var last = actionQueue.pending;

if (last === null) {
// There are no pending actions; this is the first one. We can run
// it immediately.
var newLast = {
payload: payload,
action: actionQueue.action,
next: null // circular

};
newLast.next = actionQueue.pending = newLast;
runActionStateAction(actionQueue, setPendingState, setState, newLast);
actionNode.next = actionQueue.pending = actionNode;
runActionStateAction(actionQueue, actionNode);
} else {
// There's already an action running. Add to the queue.
var first = last.next;
var _newLast = {
payload: payload,
action: actionQueue.action,
next: first
};
actionQueue.pending = last.next = _newLast;
actionNode.next = first;
actionQueue.pending = last.next = actionNode;
}
}

function runActionStateAction(actionQueue, setPendingState, setState, node) {
// This is a fork of startTransition
var prevTransition = ReactSharedInternals.T;
var currentTransition = {};
ReactSharedInternals.T = currentTransition;

{
ReactSharedInternals.T._updatedFibers = new Set();
} // Optimistically update the pending state, similar to useTransition.
// This will be reverted automatically when all actions are finished.


setPendingState(true); // `node.action` represents the action function at the time it was dispatched.
function runActionStateAction(actionQueue, node) {
// `node.action` represents the action function at the time it was dispatched.
// If this action was queued, it might be stale, i.e. it's not necessarily the
// most current implementation of the action, stored on `actionQueue`. This is
// intentional. The conceptual model for queued actions is that they are
// queued in a remote worker; the dispatch happens immediately, only the
// execution is delayed.

var action = node.action;
var payload = node.payload;
var prevState = actionQueue.state;

try {
var returnValue = action(prevState, payload);
var onStartTransitionFinish = ReactSharedInternals.S;
if (node.isTransition) {
// The original dispatch was part of a transition. We restore its
// transition context here.
// This is a fork of startTransition
var prevTransition = ReactSharedInternals.T;
var currentTransition = {};
ReactSharedInternals.T = currentTransition;

if (onStartTransitionFinish !== null) {
onStartTransitionFinish(currentTransition, returnValue);
{
ReactSharedInternals.T._updatedFibers = new Set();
}

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

thenable.then(function (nextState) {
actionQueue.state = nextState;
finishRunningActionStateAction(actionQueue, setPendingState, setState);
}, function () {
return finishRunningActionStateAction(actionQueue, setPendingState, setState);
});
setState(thenable);
} else {
setState(returnValue);
var nextState = returnValue;
actionQueue.state = nextState;
finishRunningActionStateAction(actionQueue, setPendingState, setState);
}
} catch (error) {
// This is a trick to get the `useActionState` hook to rethrow the error.
// When it unwraps the thenable with the `use` algorithm, the error
// will be thrown.
var rejectedThenable = {
then: function () {},
status: 'rejected',
reason: error // $FlowFixMe: Not sure why this doesn't work
if (onStartTransitionFinish !== null) {
onStartTransitionFinish(currentTransition, returnValue);
}

};
setState(rejectedThenable);
finishRunningActionStateAction(actionQueue, setPendingState, setState);
} finally {
ReactSharedInternals.T = prevTransition;
handleActionReturnValue(actionQueue, node, returnValue);
} catch (error) {
onActionError(actionQueue, node, error);
} finally {
ReactSharedInternals.T = prevTransition;

{
if (prevTransition === null && currentTransition._updatedFibers) {
var updatedFibersCount = currentTransition._updatedFibers.size;
{
if (prevTransition === null && currentTransition._updatedFibers) {
var updatedFibersCount = currentTransition._updatedFibers.size;

currentTransition._updatedFibers.clear();
currentTransition._updatedFibers.clear();

if (updatedFibersCount > 10) {
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.');
if (updatedFibersCount > 10) {
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.');
}
}
}
}
} else {
// The original dispatch was not part of a transition.
try {
var _returnValue = action(prevState, payload);

handleActionReturnValue(actionQueue, node, _returnValue);
} catch (error) {
onActionError(actionQueue, node, error);
}
}
}

function handleActionReturnValue(actionQueue, node, returnValue) {
if (returnValue !== null && typeof returnValue === 'object' && // $FlowFixMe[method-unbinding]
typeof returnValue.then === 'function') {
var thenable = returnValue; // Attach a listener to read the return state of the action. As soon as
// this resolves, we can run the next action in the sequence.

thenable.then(function (nextState) {
onActionSuccess(actionQueue, node, nextState);
}, function (error) {
return onActionError(actionQueue, node, error);
});

{
if (!node.isTransition) {
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`');
}
}
} else {
var nextState = returnValue;
onActionSuccess(actionQueue, node, nextState);
}
}

function finishRunningActionStateAction(actionQueue, setPendingState, setState) {
// The action finished running. Pop it from the queue and run the next pending
// action, if there are any.
function onActionSuccess(actionQueue, actionNode, nextState) {
// The action finished running.
actionNode.status = 'fulfilled';
actionNode.value = nextState;
notifyActionListeners(actionNode);
actionQueue.state = nextState; // Pop the action from the queue and run the next pending action, if there
// are any.

var last = actionQueue.pending;

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

runActionStateAction(actionQueue, setPendingState, setState, next);
runActionStateAction(actionQueue, next);
}
}
}

function onActionError(actionQueue, actionNode, error) {
actionNode.status = 'rejected';
actionNode.reason = error;
notifyActionListeners(actionNode); // Pop the action from the queue and run the next pending action, if there
// are any.
// TODO: We should instead abort all the remaining actions in the queue.

var last = actionQueue.pending;

if (last !== null) {
var first = last.next;

if (first === last) {
// This was the last action in the queue.
actionQueue.pending = null;
} else {
// Remove the first node from the circular queue.
var next = first.next;
last.next = next; // Run the next action.

runActionStateAction(actionQueue, next);
}
}
}

function notifyActionListeners(actionNode) {
// Notify React that the action has finished.
var listeners = actionNode.listeners;

for (var i = 0; i < listeners.length; i++) {
// This is always a React internal listener, so we don't need to worry
// about it throwing.
var listener = listeners[i];
listener();
}
}

function actionStateReducer(oldState, newState) {
return newState;
}
Expand Down
Loading

0 comments on commit a56f5cf

Please sign in to comment.