Rethrow errors from form actions (#26689)

This is the next step toward full support for async form actions.

Errors thrown inside form actions should cause the form to re-render and
throw the error so it can be captured by an error boundary. The behavior
is the same if the `<form />` had an internal useTransition hook, which
is pretty much exactly how we implement it, too.

The first time an action is called, the form's HostComponent is
"upgraded" to become stateful, by lazily mounting a list of hooks. The
rest of the implementation for function components can be shared.

Because the error handling behavior added in this commit is just using
useTransition under-the-hood, it also handles pending states, too.
However, this pending state can't be observed until we add a new hook
for that purpose. I'll add this next.

DiffTrain build for commit https://github.com/facebook/react/commit/fd3fb8e3c5d1c977f4bfa73d715143804c69d4b0.
This commit is contained in:
acdlite
2023-04-21 17:36:27 +00:00
parent 58e0487956
commit 0ddb2297da
13 changed files with 70 additions and 28 deletions
@@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<a8437bee4ca7ca2a6e95eed698390cea>>
* @generated SignedSource<<968b9a9a87595c8009c33efb698508af>>
*/
'use strict';
@@ -21437,6 +21437,16 @@ function replaySuspendedUnitOfWork(unitOfWork) {
break;
}
case HostComponent: {
// Some host components are stateful (that's how we implement form
// actions) but we don't bother to reuse the memoized state because it's
// not worth the extra code. The main reason to reuse the previous hooks
// is to reuse uncached promises, but we happen to know that the only
// promises that a host component might suspend on are definitely cached
// because they are controlled by us. So don't bother.
resetHooksOnUnwind(); // Fallthrough to the next branch.
}
default: {
// Other types besides function components are reset completely before
// being replayed. Currently this only happens when a Usable type is
@@ -23838,7 +23848,7 @@ function createFiberRoot(
return root;
}
var ReactVersion = "18.3.0-next-c57a0f68a-20230421";
var ReactVersion = "18.3.0-next-fd3fb8e3c-20230421";
// Might add PROFILE later.
@@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<4f902d10ff6c4c49a6f2e765b4662a5e>>
* @generated SignedSource<<3dd5e105138064b59966612a35ec1f24>>
*/
"use strict";
@@ -6973,6 +6973,8 @@ function replaySuspendedUnitOfWork(unitOfWork) {
workInProgressRootRenderLanes
);
break;
case 5:
resetHooksOnUnwind();
default:
unwindInterruptedWork(current, unitOfWork),
(unitOfWork = workInProgress =
@@ -8599,7 +8601,7 @@ var devToolsConfig$jscomp$inline_1021 = {
throw Error("TestRenderer does not support findFiberByHostInstance()");
},
bundleType: 0,
version: "18.3.0-next-c57a0f68a-20230421",
version: "18.3.0-next-fd3fb8e3c-20230421",
rendererPackageName: "react-test-renderer"
};
var internals$jscomp$inline_1206 = {
@@ -8630,7 +8632,7 @@ var internals$jscomp$inline_1206 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-next-c57a0f68a-20230421"
reconcilerVersion: "18.3.0-next-fd3fb8e3c-20230421"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1207 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
@@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<7b0d73bfc531dfd6dc0711c8a343756d>>
* @generated SignedSource<<778f87e6da120be9b98db0d06274b299>>
*/
"use strict";
@@ -7324,6 +7324,8 @@ function replaySuspendedUnitOfWork(unitOfWork) {
workInProgressRootRenderLanes
);
break;
case 5:
resetHooksOnUnwind();
default:
unwindInterruptedWork(current, unitOfWork),
(unitOfWork = workInProgress =
@@ -9025,7 +9027,7 @@ var devToolsConfig$jscomp$inline_1063 = {
throw Error("TestRenderer does not support findFiberByHostInstance()");
},
bundleType: 0,
version: "18.3.0-next-c57a0f68a-20230421",
version: "18.3.0-next-fd3fb8e3c-20230421",
rendererPackageName: "react-test-renderer"
};
var internals$jscomp$inline_1247 = {
@@ -9056,7 +9058,7 @@ var internals$jscomp$inline_1247 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-next-c57a0f68a-20230421"
reconcilerVersion: "18.3.0-next-fd3fb8e3c-20230421"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1248 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
@@ -27,7 +27,7 @@ if (
}
"use strict";
var ReactVersion = "18.3.0-next-c57a0f68a-20230421";
var ReactVersion = "18.3.0-next-fd3fb8e3c-20230421";
// ATTENTION
// When adding new symbols to this file,
@@ -639,4 +639,4 @@ exports.useSyncExternalStore = function (
);
};
exports.useTransition = useTransition;
exports.version = "18.3.0-next-c57a0f68a-20230421";
exports.version = "18.3.0-next-fd3fb8e3c-20230421";
@@ -642,7 +642,7 @@ exports.useSyncExternalStore = function (
);
};
exports.useTransition = useTransition;
exports.version = "18.3.0-next-c57a0f68a-20230421";
exports.version = "18.3.0-next-fd3fb8e3c-20230421";
/* global __REACT_DEVTOOLS_GLOBAL_HOOK__ */
if (
@@ -1 +1 @@
c57a0f68a49d0a7fbe45e7b28820478d0fa4e32f
fd3fb8e3c5d1c977f4bfa73d715143804c69d4b0
@@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<40e2743803d8d937ee642c7e74254e74>>
* @generated SignedSource<<b3b5a76440bc189cfbc25945472cc0ca>>
*/
'use strict';
@@ -24609,6 +24609,16 @@ function replaySuspendedUnitOfWork(unitOfWork) {
break;
}
case HostComponent: {
// Some host components are stateful (that's how we implement form
// actions) but we don't bother to reuse the memoized state because it's
// not worth the extra code. The main reason to reuse the previous hooks
// is to reuse uncached promises, but we happen to know that the only
// promises that a host component might suspend on are definitely cached
// because they are controlled by us. So don't bother.
resetHooksOnUnwind(); // Fallthrough to the next branch.
}
default: {
// Other types besides function components are reset completely before
// being replayed. Currently this only happens when a Usable type is
@@ -27148,7 +27158,7 @@ function createFiberRoot(
return root;
}
var ReactVersion = "18.3.0-next-c57a0f68a-20230421";
var ReactVersion = "18.3.0-next-fd3fb8e3c-20230421";
function createPortal$1(
children,
@@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<472cb7f28d742af2e290a0816a880f33>>
* @generated SignedSource<<388ecd5d71ee2c3022a43487fedc6a5a>>
*/
"use strict";
@@ -8063,6 +8063,8 @@ function replaySuspendedUnitOfWork(unitOfWork) {
workInProgressRootRenderLanes
);
break;
case 5:
resetHooksOnUnwind();
default:
unwindInterruptedWork(current, unitOfWork),
(unitOfWork = workInProgress =
@@ -9470,7 +9472,7 @@ var roots = new Map(),
devToolsConfig$jscomp$inline_1045 = {
findFiberByHostInstance: getInstanceFromNode,
bundleType: 0,
version: "18.3.0-next-c57a0f68a-20230421",
version: "18.3.0-next-fd3fb8e3c-20230421",
rendererPackageName: "react-native-renderer",
rendererConfig: {
getInspectorDataForViewTag: function () {
@@ -9512,7 +9514,7 @@ var internals$jscomp$inline_1276 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-next-c57a0f68a-20230421"
reconcilerVersion: "18.3.0-next-fd3fb8e3c-20230421"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1277 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
@@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<eb589f3defe98e20e791e57446c3f979>>
* @generated SignedSource<<8e8a7cd3892f3b9331677bf16ba6a24b>>
*/
@@ -8660,6 +8660,8 @@ function replaySuspendedUnitOfWork(unitOfWork) {
workInProgressRootRenderLanes
);
break;
case 5:
resetHooksOnUnwind();
default:
unwindInterruptedWork(current, unitOfWork),
(unitOfWork = workInProgress =
@@ -10179,7 +10181,7 @@ var roots = new Map(),
devToolsConfig$jscomp$inline_1123 = {
findFiberByHostInstance: getInstanceFromNode,
bundleType: 0,
version: "18.3.0-next-c57a0f68a-20230421",
version: "18.3.0-next-fd3fb8e3c-20230421",
rendererPackageName: "react-native-renderer",
rendererConfig: {
getInspectorDataForViewTag: function () {
@@ -10234,7 +10236,7 @@ var roots = new Map(),
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-next-c57a0f68a-20230421"
reconcilerVersion: "18.3.0-next-fd3fb8e3c-20230421"
});
exports.createPortal = function (children, containerTag) {
return createPortal$1(
@@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<36dfe2e6840704492b6fbf0d00f5b91a>>
* @generated SignedSource<<6a9f3cb903b49266953a4dde78ca03fb>>
*/
'use strict';
@@ -25122,6 +25122,16 @@ function replaySuspendedUnitOfWork(unitOfWork) {
break;
}
case HostComponent: {
// Some host components are stateful (that's how we implement form
// actions) but we don't bother to reuse the memoized state because it's
// not worth the extra code. The main reason to reuse the previous hooks
// is to reuse uncached promises, but we happen to know that the only
// promises that a host component might suspend on are definitely cached
// because they are controlled by us. So don't bother.
resetHooksOnUnwind(); // Fallthrough to the next branch.
}
default: {
// Other types besides function components are reset completely before
// being replayed. Currently this only happens when a Usable type is
@@ -27661,7 +27671,7 @@ function createFiberRoot(
return root;
}
var ReactVersion = "18.3.0-next-c57a0f68a-20230421";
var ReactVersion = "18.3.0-next-fd3fb8e3c-20230421";
function createPortal$1(
children,
@@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<54d01cff9e9c78ffba08f73a6631a077>>
* @generated SignedSource<<04b313069e799ea7f158cb3281fe8365>>
*/
"use strict";
@@ -8315,6 +8315,8 @@ function replaySuspendedUnitOfWork(unitOfWork) {
workInProgressRootRenderLanes
);
break;
case 5:
resetHooksOnUnwind();
default:
unwindInterruptedWork(current, unitOfWork),
(unitOfWork = workInProgress =
@@ -9729,7 +9731,7 @@ var roots = new Map(),
devToolsConfig$jscomp$inline_1100 = {
findFiberByHostInstance: getInstanceFromTag,
bundleType: 0,
version: "18.3.0-next-c57a0f68a-20230421",
version: "18.3.0-next-fd3fb8e3c-20230421",
rendererPackageName: "react-native-renderer",
rendererConfig: {
getInspectorDataForViewTag: function () {
@@ -9771,7 +9773,7 @@ var internals$jscomp$inline_1345 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-next-c57a0f68a-20230421"
reconcilerVersion: "18.3.0-next-fd3fb8e3c-20230421"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1346 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
@@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<1ac72a305be24eff51144b88fe9c565d>>
* @generated SignedSource<<9b14144dbccddb7d3273b95ff3f77a1c>>
*/
@@ -8912,6 +8912,8 @@ function replaySuspendedUnitOfWork(unitOfWork) {
workInProgressRootRenderLanes
);
break;
case 5:
resetHooksOnUnwind();
default:
unwindInterruptedWork(current, unitOfWork),
(unitOfWork = workInProgress =
@@ -10438,7 +10440,7 @@ var roots = new Map(),
devToolsConfig$jscomp$inline_1178 = {
findFiberByHostInstance: getInstanceFromTag,
bundleType: 0,
version: "18.3.0-next-c57a0f68a-20230421",
version: "18.3.0-next-fd3fb8e3c-20230421",
rendererPackageName: "react-native-renderer",
rendererConfig: {
getInspectorDataForViewTag: function () {
@@ -10493,7 +10495,7 @@ var roots = new Map(),
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "18.3.0-next-c57a0f68a-20230421"
reconcilerVersion: "18.3.0-next-fd3fb8e3c-20230421"
});
exports.__SECRET_INTERNALS_DO_NOT_USE_OR_YOU_WILL_BE_FIRED = {
computeComponentStackForErrorReporting: function (reactTag) {