diff --git a/packages/react-dom/src/__tests__/ReactDOMForm-test.js b/packages/react-dom/src/__tests__/ReactDOMForm-test.js
index e38dc244f652d..fbae2805bad88 100644
--- a/packages/react-dom/src/__tests__/ReactDOMForm-test.js
+++ b/packages/react-dom/src/__tests__/ReactDOMForm-test.js
@@ -1237,14 +1237,12 @@ describe('ReactDOMForm', () => {
// @gate enableAsyncActions
test('useActionState: error handling (sync action)', async () => {
- let resetErrorBoundary;
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
- resetErrorBoundary = () => this.setState({error: null});
if (this.state.error !== null) {
return ;
}
@@ -1284,31 +1282,16 @@ describe('ReactDOMForm', () => {
'Caught an error: Oops!',
]);
expect(container.textContent).toBe('Caught an error: Oops!');
-
- // Reset the error boundary
- await act(() => resetErrorBoundary());
- assertLog(['A']);
-
- // Trigger an error again, but this time, perform another action that
- // overrides the first one and fixes the error
- await act(() => {
- startTransition(() => action('Oops!'));
- startTransition(() => action('B'));
- });
- assertLog(['Pending A', 'B']);
- expect(container.textContent).toBe('B');
});
// @gate enableAsyncActions
test('useActionState: error handling (async action)', async () => {
- let resetErrorBoundary;
class ErrorBoundary extends React.Component {
state = {error: null};
static getDerivedStateFromError(error) {
return {error};
}
render() {
- resetErrorBoundary = () => this.setState({error: null});
if (this.state.error !== null) {
return ;
}
@@ -1346,21 +1329,65 @@ describe('ReactDOMForm', () => {
await act(() => resolveText('Oops!'));
assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
expect(container.textContent).toBe('Caught an error: Oops!');
+ });
+
+ test('useActionState: when an action errors, subsequent actions are canceled', async () => {
+ class ErrorBoundary extends React.Component {
+ state = {error: null};
+ static getDerivedStateFromError(error) {
+ return {error};
+ }
+ render() {
+ if (this.state.error !== null) {
+ return ;
+ }
+ return this.props.children;
+ }
+ }
+
+ let action;
+ function App() {
+ const [state, dispatch, isPending] = useActionState(async (s, a) => {
+ Scheduler.log('Start action: ' + a);
+ const text = await getText(a);
+ if (text.endsWith('!')) {
+ throw new Error(text);
+ }
+ return text;
+ }, 'A');
+ action = dispatch;
+ const pending = isPending ? 'Pending ' : '';
+ return ;
+ }
- // Reset the error boundary
- await act(() => resetErrorBoundary());
+ const root = ReactDOMClient.createRoot(container);
+ await act(() =>
+ root.render(
+
+
+ ,
+ ),
+ );
assertLog(['A']);
- // Trigger an error again, but this time, perform another action that
- // overrides the first one and fixes the error
- await act(() => {
- startTransition(() => action('Oops!'));
- startTransition(() => action('B'));
- });
- assertLog(['Pending A']);
- await act(() => resolveText('B'));
- assertLog(['B']);
- expect(container.textContent).toBe('B');
+ await act(() => startTransition(() => action('Oops!')));
+ assertLog(['Start action: Oops!', 'Pending A']);
+
+ // Queue up another action after the one will error.
+ await act(() => startTransition(() => action('Should never run')));
+ assertLog([]);
+
+ // The first dispatch will update the pending state.
+ await act(() => resolveText('Oops!'));
+ assertLog(['Caught an error: Oops!', 'Caught an error: Oops!']);
+ expect(container.textContent).toBe('Caught an error: Oops!');
+
+ // Attempt to dispatch another action. This should not run either.
+ await act(() =>
+ startTransition(() => action('This also should never run')),
+ );
+ assertLog([]);
+ expect(container.textContent).toBe('Caught an error: Oops!');
});
// @gate enableAsyncActions
diff --git a/packages/react-reconciler/src/ReactFiberHooks.js b/packages/react-reconciler/src/ReactFiberHooks.js
index 93c676f020535..de85c8246cb6c 100644
--- a/packages/react-reconciler/src/ReactFiberHooks.js
+++ b/packages/react-reconciler/src/ReactFiberHooks.js
@@ -1965,7 +1965,9 @@ type ActionStateQueue = {
dispatch: Dispatch
,
// This is the most recent action function that was rendered. It's updated
// during the commit phase.
- action: (Awaited, P) => S,
+ // If it's null, it means the action queue errored and subsequent actions
+ // should not run.
+ action: ((Awaited, P) => S) | null,
// This is a circular linked list of pending action payloads. It incudes the
// action that is currently running.
pending: ActionStateQueueNode | null,
@@ -2004,9 +2006,15 @@ function dispatchActionState(
throw new Error('Cannot update form state while rendering.');
}
+ const currentAction = actionQueue.action;
+ if (currentAction === null) {
+ // An earlier action errored. Subsequent actions should not run.
+ return;
+ }
+
const actionNode: ActionStateQueueNode = {
payload,
- action: actionQueue.action,
+ action: currentAction,
next: (null: any), // circular
isTransition: true,
@@ -2189,28 +2197,21 @@ function onActionError(
actionNode: ActionStateQueueNode,
error: mixed,
) {
- 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.
+ // Mark all the following actions as rejected.
const last = actionQueue.pending;
+ actionQueue.pending = null;
if (last !== null) {
const 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.
- const next = first.next;
- last.next = next;
-
- // Run the next action.
- runActionStateAction(actionQueue, next);
- }
+ do {
+ actionNode.status = 'rejected';
+ actionNode.reason = error;
+ notifyActionListeners(actionNode);
+ actionNode = actionNode.next;
+ } while (actionNode !== first);
}
+
+ // Prevent subsequent actions from being dispatched.
+ actionQueue.action = null;
}
function notifyActionListeners(actionNode: ActionStateQueueNode) {