Skip to content

Commit df2c19f

Browse files
committed
useFormState: Compare action signatures when reusing form state (#27370)
During an MPA form submission, useFormState should only reuse the form state if same action is passed both times. (We also compare the key paths.) We compare the identity of the inner closure function, disregarding the value of the bound arguments. That way you can pass an inline Server Action closure: ```js function FormContainer({maxLength}) { function submitAction(prevState, formData) { 'use server' if (formData.get('field').length > maxLength) { return { errorMsg: 'Too many characters' }; } // ... } return <Form submitAction={submitAction} /> } ``` DiffTrain build for [95c9554](95c9554)
1 parent 43fc2ba commit df2c19f

8 files changed

+321
-219
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
612b2b6601abb844248c384d1e288bb824b180b7
1+
95c9554bc72813b0ee2b028774bb7cf0482887ba

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

Lines changed: 61 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ if (__DEV__) {
1919
var React = require("react");
2020
var ReactDOM = require("react-dom");
2121

22-
var ReactVersion = "18.3.0-www-classic-71d6b3f3";
22+
var ReactVersion = "18.3.0-www-classic-62f810e0";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -9433,66 +9433,82 @@ function useFormState(action, initialState, permalink) {
94339433
// this component, so we can generate a unique key for each one.
94349434

94359435
var formStateHookIndex = formStateCounter++;
9436-
var request = currentlyRenderingRequest; // Append a node to the key path that represents the form state hook.
9436+
var request = currentlyRenderingRequest; // $FlowIgnore[prop-missing]
94379437

9438-
var componentKey = currentlyRenderingKeyPath;
9439-
var key = [componentKey, null, formStateHookIndex];
9440-
var keyJSON = JSON.stringify(key); // Get the form state. If we received form state from a previous page, then
9441-
// we should reuse that, if the action identity matches. Otherwise we'll use
9442-
// the initial state argument. We emit a comment marker into the stream
9443-
// that indicates whether the state was reused.
9438+
var formAction = action.$$FORM_ACTION;
94449439

9445-
var state;
9446-
var postbackFormState = getFormState(request);
9440+
if (typeof formAction === "function") {
9441+
// This is a server action. These have additional features to enable
9442+
// MPA-style form submissions with progressive enhancement.
9443+
// Determine the current form state. If we received state during an MPA form
9444+
// submission, then we will reuse that, if the action identity matches.
9445+
// Otherwise we'll use the initial state argument. We will emit a comment
9446+
// marker into the stream that indicates whether the state was reused.
9447+
var state = initialState; // Append a node to the key path that represents the form state hook.
94479448

9448-
if (postbackFormState !== null) {
9449-
var postbackKey = postbackFormState[1]; // TODO: Compare the action identity, too
9450-
// TODO: If a permalink is used, disregard the key and compare that instead.
9449+
var componentKey = currentlyRenderingKeyPath;
9450+
var key = [componentKey, null, formStateHookIndex];
9451+
var keyJSON = JSON.stringify(key);
9452+
var postbackFormState = getFormState(request); // $FlowIgnore[prop-missing]
94519453

9452-
if (keyJSON === postbackKey) {
9453-
// This was a match.
9454-
formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form.
9454+
var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL;
94559455

9456-
state = postbackFormState[0];
9457-
} else {
9458-
state = initialState;
9459-
}
9460-
} else {
9461-
// TODO: As an optimization, Fizz should only emit these markers if form
9462-
// state is passed at the root.
9463-
state = initialState;
9464-
} // Bind the state to the first argument of the action.
9456+
if (postbackFormState !== null && typeof isSignatureEqual === "function") {
9457+
var postbackKeyJSON = postbackFormState[1];
9458+
var postbackReferenceId = postbackFormState[2];
9459+
var postbackBoundArity = postbackFormState[3];
94659460

9466-
var boundAction = action.bind(null, state); // Wrap the action so the return value is void.
9461+
if (
9462+
postbackKeyJSON === keyJSON &&
9463+
isSignatureEqual.call(action, postbackReferenceId, postbackBoundArity)
9464+
) {
9465+
// This was a match
9466+
formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form.
9467+
9468+
state = postbackFormState[0];
9469+
}
9470+
} // Bind the state to the first argument of the action.
94679471

9468-
var dispatch = function (payload) {
9469-
boundAction(payload);
9470-
}; // $FlowIgnore[prop-missing]
9472+
var boundAction = action.bind(null, state); // Wrap the action so the return value is void.
94719473

9472-
if (typeof boundAction.$$FORM_ACTION === "function") {
9473-
// $FlowIgnore[prop-missing]
9474-
dispatch.$$FORM_ACTION = function (prefix) {
9474+
var dispatch = function (payload) {
9475+
boundAction(payload);
9476+
}; // $FlowIgnore[prop-missing]
9477+
9478+
if (typeof boundAction.$$FORM_ACTION === "function") {
94759479
// $FlowIgnore[prop-missing]
9476-
var metadata = boundAction.$$FORM_ACTION(prefix);
9477-
var formData = metadata.data;
9480+
dispatch.$$FORM_ACTION = function (prefix) {
9481+
var metadata = boundAction.$$FORM_ACTION(prefix);
9482+
var formData = metadata.data;
94789483

9479-
if (formData) {
9480-
formData.append("$ACTION_KEY", keyJSON);
9481-
} // Override the action URL
9484+
if (formData) {
9485+
formData.append("$ACTION_KEY", keyJSON);
9486+
} // Override the action URL
94829487

9483-
if (permalink !== undefined) {
9484-
{
9485-
checkAttributeStringCoercion(permalink, "target");
9488+
if (permalink !== undefined) {
9489+
{
9490+
checkAttributeStringCoercion(permalink, "target");
9491+
}
9492+
9493+
metadata.action = permalink + "";
94869494
}
94879495

9488-
metadata.action = permalink + "";
9489-
}
9496+
return metadata;
9497+
};
9498+
}
9499+
9500+
return [state, dispatch];
9501+
} else {
9502+
// This is not a server action, so the implementation is much simpler.
9503+
// Bind the state to the first argument of the action.
9504+
var _boundAction = action.bind(null, initialState); // Wrap the action so the return value is void.
94909505

9491-
return metadata;
9506+
var _dispatch2 = function (payload) {
9507+
_boundAction(payload);
94929508
};
9493-
}
94949509

9495-
return [state, dispatch];
9510+
return [initialState, _dispatch2];
9511+
}
94969512
}
94979513

94989514
function useId() {

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

Lines changed: 61 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ if (__DEV__) {
1919
var React = require("react");
2020
var ReactDOM = require("react-dom");
2121

22-
var ReactVersion = "18.3.0-www-modern-5b6fb822";
22+
var ReactVersion = "18.3.0-www-modern-9f6e7af7";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -9192,66 +9192,82 @@ function useFormState(action, initialState, permalink) {
91929192
// this component, so we can generate a unique key for each one.
91939193

91949194
var formStateHookIndex = formStateCounter++;
9195-
var request = currentlyRenderingRequest; // Append a node to the key path that represents the form state hook.
9195+
var request = currentlyRenderingRequest; // $FlowIgnore[prop-missing]
91969196

9197-
var componentKey = currentlyRenderingKeyPath;
9198-
var key = [componentKey, null, formStateHookIndex];
9199-
var keyJSON = JSON.stringify(key); // Get the form state. If we received form state from a previous page, then
9200-
// we should reuse that, if the action identity matches. Otherwise we'll use
9201-
// the initial state argument. We emit a comment marker into the stream
9202-
// that indicates whether the state was reused.
9197+
var formAction = action.$$FORM_ACTION;
92039198

9204-
var state;
9205-
var postbackFormState = getFormState(request);
9199+
if (typeof formAction === "function") {
9200+
// This is a server action. These have additional features to enable
9201+
// MPA-style form submissions with progressive enhancement.
9202+
// Determine the current form state. If we received state during an MPA form
9203+
// submission, then we will reuse that, if the action identity matches.
9204+
// Otherwise we'll use the initial state argument. We will emit a comment
9205+
// marker into the stream that indicates whether the state was reused.
9206+
var state = initialState; // Append a node to the key path that represents the form state hook.
92069207

9207-
if (postbackFormState !== null) {
9208-
var postbackKey = postbackFormState[1]; // TODO: Compare the action identity, too
9209-
// TODO: If a permalink is used, disregard the key and compare that instead.
9208+
var componentKey = currentlyRenderingKeyPath;
9209+
var key = [componentKey, null, formStateHookIndex];
9210+
var keyJSON = JSON.stringify(key);
9211+
var postbackFormState = getFormState(request); // $FlowIgnore[prop-missing]
92109212

9211-
if (keyJSON === postbackKey) {
9212-
// This was a match.
9213-
formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form.
9213+
var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL;
92149214

9215-
state = postbackFormState[0];
9216-
} else {
9217-
state = initialState;
9218-
}
9219-
} else {
9220-
// TODO: As an optimization, Fizz should only emit these markers if form
9221-
// state is passed at the root.
9222-
state = initialState;
9223-
} // Bind the state to the first argument of the action.
9215+
if (postbackFormState !== null && typeof isSignatureEqual === "function") {
9216+
var postbackKeyJSON = postbackFormState[1];
9217+
var postbackReferenceId = postbackFormState[2];
9218+
var postbackBoundArity = postbackFormState[3];
92249219

9225-
var boundAction = action.bind(null, state); // Wrap the action so the return value is void.
9220+
if (
9221+
postbackKeyJSON === keyJSON &&
9222+
isSignatureEqual.call(action, postbackReferenceId, postbackBoundArity)
9223+
) {
9224+
// This was a match
9225+
formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form.
9226+
9227+
state = postbackFormState[0];
9228+
}
9229+
} // Bind the state to the first argument of the action.
92269230

9227-
var dispatch = function (payload) {
9228-
boundAction(payload);
9229-
}; // $FlowIgnore[prop-missing]
9231+
var boundAction = action.bind(null, state); // Wrap the action so the return value is void.
92309232

9231-
if (typeof boundAction.$$FORM_ACTION === "function") {
9232-
// $FlowIgnore[prop-missing]
9233-
dispatch.$$FORM_ACTION = function (prefix) {
9233+
var dispatch = function (payload) {
9234+
boundAction(payload);
9235+
}; // $FlowIgnore[prop-missing]
9236+
9237+
if (typeof boundAction.$$FORM_ACTION === "function") {
92349238
// $FlowIgnore[prop-missing]
9235-
var metadata = boundAction.$$FORM_ACTION(prefix);
9236-
var formData = metadata.data;
9239+
dispatch.$$FORM_ACTION = function (prefix) {
9240+
var metadata = boundAction.$$FORM_ACTION(prefix);
9241+
var formData = metadata.data;
92379242

9238-
if (formData) {
9239-
formData.append("$ACTION_KEY", keyJSON);
9240-
} // Override the action URL
9243+
if (formData) {
9244+
formData.append("$ACTION_KEY", keyJSON);
9245+
} // Override the action URL
92419246

9242-
if (permalink !== undefined) {
9243-
{
9244-
checkAttributeStringCoercion(permalink, "target");
9247+
if (permalink !== undefined) {
9248+
{
9249+
checkAttributeStringCoercion(permalink, "target");
9250+
}
9251+
9252+
metadata.action = permalink + "";
92459253
}
92469254

9247-
metadata.action = permalink + "";
9248-
}
9255+
return metadata;
9256+
};
9257+
}
9258+
9259+
return [state, dispatch];
9260+
} else {
9261+
// This is not a server action, so the implementation is much simpler.
9262+
// Bind the state to the first argument of the action.
9263+
var _boundAction = action.bind(null, initialState); // Wrap the action so the return value is void.
92499264

9250-
return metadata;
9265+
var _dispatch2 = function (payload) {
9266+
_boundAction(payload);
92519267
};
9252-
}
92539268

9254-
return [state, dispatch];
9269+
return [initialState, _dispatch2];
9270+
}
92559271
}
92569272

92579273
function useId() {

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

Lines changed: 46 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -2738,32 +2738,50 @@ function useOptimistic(passthrough) {
27382738
return [passthrough, unsupportedSetOptimisticState];
27392739
}
27402740
function useFormState(action, initialState, permalink) {
2741-
function dispatch(payload) {
2742-
boundAction(payload);
2743-
}
27442741
resolveCurrentlyRenderingComponent();
27452742
var formStateHookIndex = formStateCounter++,
2746-
request = currentlyRenderingRequest,
2747-
keyJSON = JSON.stringify([
2743+
request = currentlyRenderingRequest;
2744+
if ("function" === typeof action.$$FORM_ACTION) {
2745+
var keyJSON = JSON.stringify([
27482746
currentlyRenderingKeyPath,
27492747
null,
27502748
formStateHookIndex
27512749
]);
2752-
request = request.formState;
2753-
null !== request &&
2754-
keyJSON === request[1] &&
2755-
((formStateMatchingIndex = formStateHookIndex),
2756-
(initialState = request[0]));
2757-
var boundAction = action.bind(null, initialState);
2758-
"function" === typeof boundAction.$$FORM_ACTION &&
2759-
(dispatch.$$FORM_ACTION = function (prefix) {
2760-
prefix = boundAction.$$FORM_ACTION(prefix);
2761-
var formData = prefix.data;
2762-
formData && formData.append("$ACTION_KEY", keyJSON);
2763-
void 0 !== permalink && (prefix.action = permalink + "");
2764-
return prefix;
2765-
});
2766-
return [initialState, dispatch];
2750+
request = request.formState;
2751+
var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL;
2752+
if (null !== request && "function" === typeof isSignatureEqual) {
2753+
var postbackReferenceId = request[2],
2754+
postbackBoundArity = request[3];
2755+
request[1] === keyJSON &&
2756+
isSignatureEqual.call(
2757+
action,
2758+
postbackReferenceId,
2759+
postbackBoundArity
2760+
) &&
2761+
((formStateMatchingIndex = formStateHookIndex),
2762+
(initialState = request[0]));
2763+
}
2764+
var boundAction = action.bind(null, initialState);
2765+
action = function (payload) {
2766+
boundAction(payload);
2767+
};
2768+
"function" === typeof boundAction.$$FORM_ACTION &&
2769+
(action.$$FORM_ACTION = function (prefix) {
2770+
prefix = boundAction.$$FORM_ACTION(prefix);
2771+
var formData = prefix.data;
2772+
formData && formData.append("$ACTION_KEY", keyJSON);
2773+
void 0 !== permalink && (prefix.action = permalink + "");
2774+
return prefix;
2775+
});
2776+
return [initialState, action];
2777+
}
2778+
var boundAction$10 = action.bind(null, initialState);
2779+
return [
2780+
initialState,
2781+
function (payload) {
2782+
boundAction$10(payload);
2783+
}
2784+
];
27672785
}
27682786
function unwrapThenable(thenable) {
27692787
var index = thenableIndexCounter;
@@ -4351,13 +4369,13 @@ function flushCompletedQueues(request, destination) {
43514369
completedBoundaries.splice(0, i);
43524370
var partialBoundaries = request.partialBoundaries;
43534371
for (i = 0; i < partialBoundaries.length; i++) {
4354-
var boundary$15 = partialBoundaries[i];
4372+
var boundary$17 = partialBoundaries[i];
43554373
a: {
43564374
clientRenderedBoundaries = request;
43574375
boundary = destination;
43584376
clientRenderedBoundaries.renderState.boundaryResources =
4359-
boundary$15.resources;
4360-
var completedSegments = boundary$15.completedSegments;
4377+
boundary$17.resources;
4378+
var completedSegments = boundary$17.completedSegments;
43614379
for (
43624380
resumableState$jscomp$1 = 0;
43634381
resumableState$jscomp$1 < completedSegments.length;
@@ -4367,7 +4385,7 @@ function flushCompletedQueues(request, destination) {
43674385
!flushPartiallyCompletedSegment(
43684386
clientRenderedBoundaries,
43694387
boundary,
4370-
boundary$15,
4388+
boundary$17,
43714389
completedSegments[resumableState$jscomp$1]
43724390
)
43734391
) {
@@ -4379,7 +4397,7 @@ function flushCompletedQueues(request, destination) {
43794397
completedSegments.splice(0, resumableState$jscomp$1);
43804398
JSCompiler_inline_result = writeResourcesForBoundary(
43814399
boundary,
4382-
boundary$15.resources,
4400+
boundary$17.resources,
43834401
clientRenderedBoundaries.renderState
43844402
);
43854403
}
@@ -4442,8 +4460,8 @@ function abort(request, reason) {
44424460
}
44434461
null !== request.destination &&
44444462
flushCompletedQueues(request, request.destination);
4445-
} catch (error$17) {
4446-
logRecoverableError(request, error$17), fatalError(request, error$17);
4463+
} catch (error$19) {
4464+
logRecoverableError(request, error$19), fatalError(request, error$19);
44474465
}
44484466
}
44494467
function onError() {}
@@ -4530,4 +4548,4 @@ exports.renderToString = function (children, options) {
45304548
'The server used "renderToString" which does not support Suspense. If you intended for this Suspense boundary to render the fallback content on the server consider throwing an Error somewhere within the Suspense boundary. If you intended to have the server wait for the suspended component please switch to "renderToReadableStream" which supports Suspense on the server'
45314549
);
45324550
};
4533-
exports.version = "18.3.0-www-classic-53a2640c";
4551+
exports.version = "18.3.0-www-classic-aa5cc76a";

0 commit comments

Comments
 (0)