Skip to content

Commit

Permalink
Automatically reset forms after action finishes (#28804)
Browse files Browse the repository at this point in the history
This updates the behavior of form actions to automatically reset the
form's uncontrolled inputs after the action finishes.

This is a frequent feature request for people using actions and it
aligns the behavior of client-side form submissions more closely with
MPA form submissions.

It has no impact on controlled form inputs. It's the same as if you
called `form.reset()` manually, except React handles the timing of when
the reset happens, which is tricky/impossible to get exactly right in
userspace.

The reset shouldn't happen until the UI has updated with the result of
the action. So, resetting inside the action is too early.

Resetting in `useEffect` is better, but it's later than ideal because
any effects that run before it will observe the state of the form before
it's been reset.

It needs to happen in the mutation phase of the transition. More
specifically, after all the DOM mutations caused by the transition have
been applied. That way the `defaultValue` of the inputs are updated
before the values are reset. The idea is that the `defaultValue`
represents the current, canonical value sent by the server.

Note: this change has no effect on form submissions that aren't
triggered by an action.

DiffTrain build for commit 41950d1.
  • Loading branch information
acdlite committed Apr 10, 2024
1 parent d71465b commit bd1135f
Show file tree
Hide file tree
Showing 10 changed files with 186 additions and 70 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<881e78352310ebeea1f473a89441e0c9>>
* @generated SignedSource<<8c677055ecc6af7b5a69924e87081621>>
*/

"use strict";
Expand Down Expand Up @@ -515,6 +515,7 @@ if (__DEV__) {
var ScheduleRetry = StoreConsistency;
var ShouldSuspendCommit = Visibility;
var DidDefer = ContentReset;
var FormReset = Snapshot;
var LifecycleEffectMask =
Passive$1 | Update | Callback | Ref | Snapshot | StoreConsistency; // Union of all commit flags (flags with the lifetime of a particular commit)

Expand Down Expand Up @@ -573,7 +574,8 @@ if (__DEV__) {
ContentReset |
Ref |
Hydrating |
Visibility;
Visibility |
FormReset;
var LayoutMask = Update | Callback | Ref | Visibility; // TODO: Split into PassiveMountMask and PassiveUnmountMask

var PassiveMask = Passive$1 | Visibility | ChildDeletion; // Union of tags that don't get reset on clones.
Expand Down Expand Up @@ -8335,13 +8337,29 @@ if (__DEV__) {
var _dispatcher$useState = dispatcher.useState(),
maybeThenable = _dispatcher$useState[0];

var nextState;

if (typeof maybeThenable.then === "function") {
var thenable = maybeThenable;
return useThenable(thenable);
nextState = useThenable(thenable);
} else {
var status = maybeThenable;
return status;
nextState = status;
} // The "reset state" is an object. If it changes, that means something
// requested that we reset the form.

var _dispatcher$useState2 = dispatcher.useState(),
nextResetState = _dispatcher$useState2[0];

var prevResetState =
currentHook !== null ? currentHook.memoizedState : null;

if (prevResetState !== nextResetState) {
// Schedule a form reset
currentlyRenderingFiber$1.flags |= FormReset;
}

return nextState;
}
function bailoutHooks(current, workInProgress, lanes) {
workInProgress.updateQueue = current.updateQueue; // TODO: Don't need to reset the flags here, because they're reset in the
Expand Down Expand Up @@ -18802,7 +18820,7 @@ if (__DEV__) {
// Allows us to avoid traversing the return path to find the nearest Offscreen ancestor.

var offscreenSubtreeIsHidden = false;
var offscreenSubtreeWasHidden = false;
var offscreenSubtreeWasHidden = false; // Used to track if a form needs to be reset at the end of the mutation phase.
var PossiblyWeakSet = typeof WeakSet === "function" ? WeakSet : Set;
var nextEffect = null; // Used for Profiling builds to track updaters.

Expand Down Expand Up @@ -20735,6 +20753,19 @@ if (__DEV__) {
}
}
}

if (flags & FormReset) {
{
if (finishedWork.type !== "form") {
// Paranoid coding. In case we accidentally start using the
// FormReset bit for something else.
error(
"Unexpected host component type. Expected a form. This is a " +
"bug in React."
);
}
}
}
}

return;
Expand Down Expand Up @@ -26601,7 +26632,7 @@ if (__DEV__) {
return root;
}

var ReactVersion = "19.0.0-canary-89546c49";
var ReactVersion = "19.0.0-canary-dec39fbe";

/*
* The `'' + value` pattern (used in perf-sensitive code) throws for Symbol
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<5159750d4ca65c393d679d370a71d7cb>>
* @generated SignedSource<<b41ef07452ddc0ccf5f4ca65890007ab>>
*/

"use strict";
Expand Down Expand Up @@ -2217,10 +2217,16 @@ function renderWithHooksAgain(workInProgress, Component, props, secondArg) {
return children;
}
function TransitionAwareHostComponent() {
var maybeThenable = ReactSharedInternals.H.useState()[0];
return "function" === typeof maybeThenable.then
? useThenable(maybeThenable)
: maybeThenable;
var dispatcher = ReactSharedInternals.H,
maybeThenable = dispatcher.useState()[0];
maybeThenable =
"function" === typeof maybeThenable.then
? useThenable(maybeThenable)
: maybeThenable;
dispatcher = dispatcher.useState()[0];
(null !== currentHook ? currentHook.memoizedState : null) !== dispatcher &&
(currentlyRenderingFiber$1.flags |= 1024);
return maybeThenable;
}
function bailoutHooks(current, workInProgress, lanes) {
workInProgress.updateQueue = current.updateQueue;
Expand Down Expand Up @@ -6508,7 +6514,7 @@ function recursivelyTraverseMutationEffects(root$jscomp$0, parentFiber) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
if (parentFiber.subtreeFlags & 12854)
if (parentFiber.subtreeFlags & 13878)
for (parentFiber = parentFiber.child; null !== parentFiber; )
commitMutationEffectsOnFiber(parentFiber, root$jscomp$0),
(parentFiber = parentFiber.sibling);
Expand Down Expand Up @@ -9124,7 +9130,7 @@ var devToolsConfig$jscomp$inline_1019 = {
throw Error("TestRenderer does not support findFiberByHostInstance()");
},
bundleType: 0,
version: "19.0.0-canary-df9de31a",
version: "19.0.0-canary-ffe8e2ea",
rendererPackageName: "react-test-renderer"
};
var internals$jscomp$inline_1238 = {
Expand Down Expand Up @@ -9155,7 +9161,7 @@ var internals$jscomp$inline_1238 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "19.0.0-canary-df9de31a"
reconcilerVersion: "19.0.0-canary-ffe8e2ea"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1239 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<21f520424802b9edb8fc0a8c96178e1c>>
* @generated SignedSource<<c75c1cb22c2153409d78bbb0e058c921>>
*/

"use strict";
Expand Down Expand Up @@ -2305,10 +2305,16 @@ function renderWithHooksAgain(workInProgress, Component, props, secondArg) {
return children;
}
function TransitionAwareHostComponent() {
var maybeThenable = ReactSharedInternals.H.useState()[0];
return "function" === typeof maybeThenable.then
? useThenable(maybeThenable)
: maybeThenable;
var dispatcher = ReactSharedInternals.H,
maybeThenable = dispatcher.useState()[0];
maybeThenable =
"function" === typeof maybeThenable.then
? useThenable(maybeThenable)
: maybeThenable;
dispatcher = dispatcher.useState()[0];
(null !== currentHook ? currentHook.memoizedState : null) !== dispatcher &&
(currentlyRenderingFiber$1.flags |= 1024);
return maybeThenable;
}
function bailoutHooks(current, workInProgress, lanes) {
workInProgress.updateQueue = current.updateQueue;
Expand Down Expand Up @@ -6934,7 +6940,7 @@ function recursivelyTraverseMutationEffects(root$jscomp$0, parentFiber) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
if (parentFiber.subtreeFlags & 12854)
if (parentFiber.subtreeFlags & 13878)
for (parentFiber = parentFiber.child; null !== parentFiber; )
commitMutationEffectsOnFiber(parentFiber, root$jscomp$0),
(parentFiber = parentFiber.sibling);
Expand Down Expand Up @@ -9762,7 +9768,7 @@ var devToolsConfig$jscomp$inline_1082 = {
throw Error("TestRenderer does not support findFiberByHostInstance()");
},
bundleType: 0,
version: "19.0.0-canary-12ee58d1",
version: "19.0.0-canary-1a91a1bd",
rendererPackageName: "react-test-renderer"
};
(function (internals) {
Expand Down Expand Up @@ -9806,7 +9812,7 @@ var devToolsConfig$jscomp$inline_1082 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "19.0.0-canary-12ee58d1"
reconcilerVersion: "19.0.0-canary-1a91a1bd"
});
exports._Scheduler = Scheduler;
exports.act = act;
Expand Down
Original file line number Diff line number Diff line change
@@ -1 +1 @@
ed3c65caf042f75fe2fdc2a5e568a9624c6175fb
41950d14a538aa7411b00b28bcd94ae95a45976e
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<a4571cdfd524cd72b685d2a3adeafd2c>>
* @generated SignedSource<<9e85187f59d83f3fcc6ccc4cb04215d8>>
*/

"use strict";
Expand Down Expand Up @@ -3055,6 +3055,7 @@ to return true:wantsResponderID| |
var ScheduleRetry = StoreConsistency;
var ShouldSuspendCommit = Visibility;
var DidDefer = ContentReset;
var FormReset = Snapshot;
var LifecycleEffectMask =
Passive$1 | Update | Callback | Ref | Snapshot | StoreConsistency; // Union of all commit flags (flags with the lifetime of a particular commit)

Expand Down Expand Up @@ -3113,7 +3114,8 @@ to return true:wantsResponderID| |
ContentReset |
Ref |
Hydrating |
Visibility;
Visibility |
FormReset;
var LayoutMask = Update | Callback | Ref | Visibility; // TODO: Split into PassiveMountMask and PassiveUnmountMask

var PassiveMask = Passive$1 | Visibility | ChildDeletion; // Union of tags that don't get reset on clones.
Expand Down Expand Up @@ -5031,7 +5033,7 @@ to return true:wantsResponderID| |
function waitForCommitToBeReady() {
return null;
}
var NotPendingTransition = null; // -------------------
var NotPendingTransition = null;
// Microtasks
// -------------------

Expand Down Expand Up @@ -11815,13 +11817,29 @@ to return true:wantsResponderID| |
var _dispatcher$useState = dispatcher.useState(),
maybeThenable = _dispatcher$useState[0];

var nextState;

if (typeof maybeThenable.then === "function") {
var thenable = maybeThenable;
return useThenable(thenable);
nextState = useThenable(thenable);
} else {
var status = maybeThenable;
return status;
nextState = status;
} // The "reset state" is an object. If it changes, that means something
// requested that we reset the form.

var _dispatcher$useState2 = dispatcher.useState(),
nextResetState = _dispatcher$useState2[0];

var prevResetState =
currentHook !== null ? currentHook.memoizedState : null;

if (prevResetState !== nextResetState) {
// Schedule a form reset
currentlyRenderingFiber$1.flags |= FormReset;
}

return nextState;
}
function bailoutHooks(current, workInProgress, lanes) {
workInProgress.updateQueue = current.updateQueue; // TODO: Don't need to reset the flags here, because they're reset in the
Expand Down Expand Up @@ -22636,7 +22654,7 @@ to return true:wantsResponderID| |
// Allows us to avoid traversing the return path to find the nearest Offscreen ancestor.

var offscreenSubtreeIsHidden = false;
var offscreenSubtreeWasHidden = false;
var offscreenSubtreeWasHidden = false; // Used to track if a form needs to be reset at the end of the mutation phase.
var PossiblyWeakSet = typeof WeakSet === "function" ? WeakSet : Set;
var nextEffect = null; // Used for Profiling builds to track updaters.

Expand Down Expand Up @@ -30248,7 +30266,7 @@ to return true:wantsResponderID| |
return root;
}

var ReactVersion = "19.0.0-canary-704f6d1b";
var ReactVersion = "19.0.0-canary-665179ad";

/*
* The `'' + value` pattern (used in perf-sensitive code) throws for Symbol
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
* @noflow
* @nolint
* @preventMunge
* @generated SignedSource<<2e33e857cb3eee98d5e57d1509811f8c>>
* @generated SignedSource<<acaefb2daefd1d22c7e94851b8d92a8a>>
*/

"use strict";
Expand Down Expand Up @@ -3741,10 +3741,16 @@ function renderWithHooksAgain(workInProgress, Component, props, secondArg) {
}
function TransitionAwareHostComponent() {
if (!enableAsyncActions) throw Error("Not implemented.");
var maybeThenable = ReactSharedInternals.H.useState()[0];
return "function" === typeof maybeThenable.then
? useThenable(maybeThenable)
: maybeThenable;
var dispatcher = ReactSharedInternals.H,
maybeThenable = dispatcher.useState()[0];
maybeThenable =
"function" === typeof maybeThenable.then
? useThenable(maybeThenable)
: maybeThenable;
dispatcher = dispatcher.useState()[0];
(null !== currentHook ? currentHook.memoizedState : null) !== dispatcher &&
(currentlyRenderingFiber$1.flags |= 1024);
return maybeThenable;
}
function bailoutHooks(current, workInProgress, lanes) {
workInProgress.updateQueue = current.updateQueue;
Expand Down Expand Up @@ -7039,7 +7045,7 @@ function doesRequireClone(current, completedWork) {
if (null !== current && current.child === completedWork.child) return !1;
if (0 !== (completedWork.flags & 16)) return !0;
for (current = completedWork.child; null !== current; ) {
if (0 !== (current.flags & 12854) || 0 !== (current.subtreeFlags & 12854))
if (0 !== (current.flags & 13878) || 0 !== (current.subtreeFlags & 13878))
return !0;
current = current.sibling;
}
Expand Down Expand Up @@ -8202,7 +8208,7 @@ function recursivelyTraverseMutationEffects(root, parentFiber) {
captureCommitPhaseError(childToDelete, parentFiber, error);
}
}
if (parentFiber.subtreeFlags & 12854)
if (parentFiber.subtreeFlags & 13878)
for (parentFiber = parentFiber.child; null !== parentFiber; )
commitMutationEffectsOnFiber(parentFiber, root),
(parentFiber = parentFiber.sibling);
Expand Down Expand Up @@ -10577,7 +10583,7 @@ var roots = new Map(),
devToolsConfig$jscomp$inline_1099 = {
findFiberByHostInstance: getInstanceFromNode,
bundleType: 0,
version: "19.0.0-canary-a19c8da4",
version: "19.0.0-canary-f39e5a74",
rendererPackageName: "react-native-renderer",
rendererConfig: {
getInspectorDataForInstance: getInspectorDataForInstance,
Expand Down Expand Up @@ -10620,7 +10626,7 @@ var internals$jscomp$inline_1366 = {
scheduleRoot: null,
setRefreshHandler: null,
getCurrentFiber: null,
reconcilerVersion: "19.0.0-canary-a19c8da4"
reconcilerVersion: "19.0.0-canary-f39e5a74"
};
if ("undefined" !== typeof __REACT_DEVTOOLS_GLOBAL_HOOK__) {
var hook$jscomp$inline_1367 = __REACT_DEVTOOLS_GLOBAL_HOOK__;
Expand Down
Loading

0 comments on commit bd1135f

Please sign in to comment.