Skip to content

Commit 97bcd7f

Browse files
committed
useFormState: MPA submissions to a different page (#27372)
The permalink option of useFormState controls which page the form is submitted to during an MPA form submission (i.e. a submission that happens before hydration, or when JS is disabled). If the same useFormState appears on the resulting page, and the permalink option matches, it should receive the form state from the submission despite the fact that the keypaths do not match. So the logic for whether a form state instance is considered a match is: - Both instances must be passed the same action signature - If a permalink is provided, the permalinks must match. - If a permalink is not provided, the keypaths must match. Currently, if there are multiple matching useFormStates, they will all match and receive the form state. We should probably only match the first one, and/or warn when this happens. I've left this as a TODO for now, pending further discussion. DiffTrain build for [caa716d](caa716d)
1 parent 1fddce3 commit 97bcd7f

8 files changed

+216
-108
lines changed

compiled/facebook-www/REVISION

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
a6e4791b11816374d015eb4531a82e6cf209c7f2
1+
caa716d50bdeef3a1ac5e3e0cfcc14f4d91f2028

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

Lines changed: 49 additions & 18 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-f2b91051";
22+
var ReactVersion = "18.3.0-www-classic-360a070f";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -9427,6 +9427,16 @@ function useOptimistic(passthrough, reducer) {
94279427
return [passthrough, unsupportedSetOptimisticState];
94289428
}
94299429

9430+
function createPostbackFormStateKey(permalink, componentKeyPath, hookIndex) {
9431+
if (permalink !== undefined) {
9432+
return "p" + permalink;
9433+
} else {
9434+
// Append a node to the key path that represents the form state hook.
9435+
var keyPath = [componentKeyPath, null, hookIndex];
9436+
return "k" + JSON.stringify(keyPath);
9437+
}
9438+
}
9439+
94309440
function useFormState(action, initialState, permalink) {
94319441
resolveCurrentlyRenderingComponent(); // Count the number of useFormState hooks per component. We also use this to
94329442
// track the position of this useFormState hook relative to the other ones in
@@ -9440,32 +9450,43 @@ function useFormState(action, initialState, permalink) {
94409450
if (typeof formAction === "function") {
94419451
// This is a server action. These have additional features to enable
94429452
// MPA-style form submissions with progressive enhancement.
9443-
// Determine the current form state. If we received state during an MPA form
9453+
// TODO: If the same permalink is passed to multiple useFormStates, and
9454+
// they all have the same action signature, Fizz will pass the postback
9455+
// state to all of them. We should probably only pass it to the first one,
9456+
// and/or warn.
9457+
// The key is lazily generated and deduped so the that the keypath doesn't
9458+
// get JSON.stringify-ed unnecessarily, and at most once.
9459+
var nextPostbackStateKey = null; // Determine the current form state. If we received state during an MPA form
94449460
// submission, then we will reuse that, if the action identity matches.
94459461
// Otherwise we'll use the initial state argument. We will emit a comment
94469462
// 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.
94489463

9449-
var componentKey = currentlyRenderingKeyPath;
9450-
var key = [componentKey, null, formStateHookIndex];
9451-
var keyJSON = JSON.stringify(key);
9464+
var state = initialState;
9465+
var componentKeyPath = currentlyRenderingKeyPath;
94529466
var postbackFormState = getFormState(request); // $FlowIgnore[prop-missing]
94539467

94549468
var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL;
94559469

94569470
if (postbackFormState !== null && typeof isSignatureEqual === "function") {
9457-
var postbackKeyJSON = postbackFormState[1];
9471+
var postbackKey = postbackFormState[1];
94589472
var postbackReferenceId = postbackFormState[2];
94599473
var postbackBoundArity = postbackFormState[3];
94609474

94619475
if (
9462-
postbackKeyJSON === keyJSON &&
94639476
isSignatureEqual.call(action, postbackReferenceId, postbackBoundArity)
94649477
) {
9465-
// This was a match
9466-
formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form.
9478+
nextPostbackStateKey = createPostbackFormStateKey(
9479+
permalink,
9480+
componentKeyPath,
9481+
formStateHookIndex
9482+
);
94679483

9468-
state = postbackFormState[0];
9484+
if (postbackKey === nextPostbackStateKey) {
9485+
// This was a match
9486+
formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form.
9487+
9488+
state = postbackFormState[0];
9489+
}
94699490
}
94709491
} // Bind the state to the first argument of the action.
94719492

@@ -9478,19 +9499,29 @@ function useFormState(action, initialState, permalink) {
94789499
if (typeof boundAction.$$FORM_ACTION === "function") {
94799500
// $FlowIgnore[prop-missing]
94809501
dispatch.$$FORM_ACTION = function (prefix) {
9481-
var metadata = boundAction.$$FORM_ACTION(prefix);
9482-
var formData = metadata.data;
9483-
9484-
if (formData) {
9485-
formData.append("$ACTION_KEY", keyJSON);
9486-
} // Override the action URL
9502+
var metadata = boundAction.$$FORM_ACTION(prefix); // Override the action URL
94879503

94889504
if (permalink !== undefined) {
94899505
{
94909506
checkAttributeStringCoercion(permalink, "target");
94919507
}
94929508

9493-
metadata.action = permalink + "";
9509+
permalink += "";
9510+
metadata.action = permalink;
9511+
}
9512+
9513+
var formData = metadata.data;
9514+
9515+
if (formData) {
9516+
if (nextPostbackStateKey === null) {
9517+
nextPostbackStateKey = createPostbackFormStateKey(
9518+
permalink,
9519+
componentKeyPath,
9520+
formStateHookIndex
9521+
);
9522+
}
9523+
9524+
formData.append("$ACTION_KEY", nextPostbackStateKey);
94949525
}
94959526

94969527
return metadata;

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

Lines changed: 49 additions & 18 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-82925a4c";
22+
var ReactVersion = "18.3.0-www-modern-b814d906";
2323

2424
// This refers to a WWW module.
2525
var warningWWW = require("warning");
@@ -9186,6 +9186,16 @@ function useOptimistic(passthrough, reducer) {
91869186
return [passthrough, unsupportedSetOptimisticState];
91879187
}
91889188

9189+
function createPostbackFormStateKey(permalink, componentKeyPath, hookIndex) {
9190+
if (permalink !== undefined) {
9191+
return "p" + permalink;
9192+
} else {
9193+
// Append a node to the key path that represents the form state hook.
9194+
var keyPath = [componentKeyPath, null, hookIndex];
9195+
return "k" + JSON.stringify(keyPath);
9196+
}
9197+
}
9198+
91899199
function useFormState(action, initialState, permalink) {
91909200
resolveCurrentlyRenderingComponent(); // Count the number of useFormState hooks per component. We also use this to
91919201
// track the position of this useFormState hook relative to the other ones in
@@ -9199,32 +9209,43 @@ function useFormState(action, initialState, permalink) {
91999209
if (typeof formAction === "function") {
92009210
// This is a server action. These have additional features to enable
92019211
// MPA-style form submissions with progressive enhancement.
9202-
// Determine the current form state. If we received state during an MPA form
9212+
// TODO: If the same permalink is passed to multiple useFormStates, and
9213+
// they all have the same action signature, Fizz will pass the postback
9214+
// state to all of them. We should probably only pass it to the first one,
9215+
// and/or warn.
9216+
// The key is lazily generated and deduped so the that the keypath doesn't
9217+
// get JSON.stringify-ed unnecessarily, and at most once.
9218+
var nextPostbackStateKey = null; // Determine the current form state. If we received state during an MPA form
92039219
// submission, then we will reuse that, if the action identity matches.
92049220
// Otherwise we'll use the initial state argument. We will emit a comment
92059221
// 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.
92079222

9208-
var componentKey = currentlyRenderingKeyPath;
9209-
var key = [componentKey, null, formStateHookIndex];
9210-
var keyJSON = JSON.stringify(key);
9223+
var state = initialState;
9224+
var componentKeyPath = currentlyRenderingKeyPath;
92119225
var postbackFormState = getFormState(request); // $FlowIgnore[prop-missing]
92129226

92139227
var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL;
92149228

92159229
if (postbackFormState !== null && typeof isSignatureEqual === "function") {
9216-
var postbackKeyJSON = postbackFormState[1];
9230+
var postbackKey = postbackFormState[1];
92179231
var postbackReferenceId = postbackFormState[2];
92189232
var postbackBoundArity = postbackFormState[3];
92199233

92209234
if (
9221-
postbackKeyJSON === keyJSON &&
92229235
isSignatureEqual.call(action, postbackReferenceId, postbackBoundArity)
92239236
) {
9224-
// This was a match
9225-
formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form.
9237+
nextPostbackStateKey = createPostbackFormStateKey(
9238+
permalink,
9239+
componentKeyPath,
9240+
formStateHookIndex
9241+
);
92269242

9227-
state = postbackFormState[0];
9243+
if (postbackKey === nextPostbackStateKey) {
9244+
// This was a match
9245+
formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form.
9246+
9247+
state = postbackFormState[0];
9248+
}
92289249
}
92299250
} // Bind the state to the first argument of the action.
92309251

@@ -9237,19 +9258,29 @@ function useFormState(action, initialState, permalink) {
92379258
if (typeof boundAction.$$FORM_ACTION === "function") {
92389259
// $FlowIgnore[prop-missing]
92399260
dispatch.$$FORM_ACTION = function (prefix) {
9240-
var metadata = boundAction.$$FORM_ACTION(prefix);
9241-
var formData = metadata.data;
9242-
9243-
if (formData) {
9244-
formData.append("$ACTION_KEY", keyJSON);
9245-
} // Override the action URL
9261+
var metadata = boundAction.$$FORM_ACTION(prefix); // Override the action URL
92469262

92479263
if (permalink !== undefined) {
92489264
{
92499265
checkAttributeStringCoercion(permalink, "target");
92509266
}
92519267

9252-
metadata.action = permalink + "";
9268+
permalink += "";
9269+
metadata.action = permalink;
9270+
}
9271+
9272+
var formData = metadata.data;
9273+
9274+
if (formData) {
9275+
if (nextPostbackStateKey === null) {
9276+
nextPostbackStateKey = createPostbackFormStateKey(
9277+
permalink,
9278+
componentKeyPath,
9279+
formStateHookIndex
9280+
);
9281+
}
9282+
9283+
formData.append("$ACTION_KEY", nextPostbackStateKey);
92539284
}
92549285

92559286
return metadata;

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

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2742,24 +2742,21 @@ function useFormState(action, initialState, permalink) {
27422742
var formStateHookIndex = formStateCounter++,
27432743
request = currentlyRenderingRequest;
27442744
if ("function" === typeof action.$$FORM_ACTION) {
2745-
var keyJSON = JSON.stringify([
2746-
currentlyRenderingKeyPath,
2747-
null,
2748-
formStateHookIndex
2749-
]);
2745+
var nextPostbackStateKey = null,
2746+
componentKeyPath = currentlyRenderingKeyPath;
27502747
request = request.formState;
27512748
var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL;
27522749
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]));
2750+
var postbackKey = request[1];
2751+
isSignatureEqual.call(action, request[2], request[3]) &&
2752+
((nextPostbackStateKey =
2753+
void 0 !== permalink
2754+
? "p" + permalink
2755+
: "k" +
2756+
JSON.stringify([componentKeyPath, null, formStateHookIndex])),
2757+
postbackKey === nextPostbackStateKey &&
2758+
((formStateMatchingIndex = formStateHookIndex),
2759+
(initialState = request[0])));
27632760
}
27642761
var boundAction = action.bind(null, initialState);
27652762
action = function (payload) {
@@ -2768,9 +2765,17 @@ function useFormState(action, initialState, permalink) {
27682765
"function" === typeof boundAction.$$FORM_ACTION &&
27692766
(action.$$FORM_ACTION = function (prefix) {
27702767
prefix = boundAction.$$FORM_ACTION(prefix);
2768+
void 0 !== permalink &&
2769+
((permalink += ""), (prefix.action = permalink));
27712770
var formData = prefix.data;
2772-
formData && formData.append("$ACTION_KEY", keyJSON);
2773-
void 0 !== permalink && (prefix.action = permalink + "");
2771+
formData &&
2772+
(null === nextPostbackStateKey &&
2773+
(nextPostbackStateKey =
2774+
void 0 !== permalink
2775+
? "p" + permalink
2776+
: "k" +
2777+
JSON.stringify([componentKeyPath, null, formStateHookIndex])),
2778+
formData.append("$ACTION_KEY", nextPostbackStateKey));
27742779
return prefix;
27752780
});
27762781
return [initialState, action];
@@ -4551,4 +4556,4 @@ exports.renderToString = function (children, options) {
45514556
'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'
45524557
);
45534558
};
4554-
exports.version = "18.3.0-www-classic-f7fe51fc";
4559+
exports.version = "18.3.0-www-classic-f04a97ef";

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

Lines changed: 23 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2734,24 +2734,21 @@ function useFormState(action, initialState, permalink) {
27342734
var formStateHookIndex = formStateCounter++,
27352735
request = currentlyRenderingRequest;
27362736
if ("function" === typeof action.$$FORM_ACTION) {
2737-
var keyJSON = JSON.stringify([
2738-
currentlyRenderingKeyPath,
2739-
null,
2740-
formStateHookIndex
2741-
]);
2737+
var nextPostbackStateKey = null,
2738+
componentKeyPath = currentlyRenderingKeyPath;
27422739
request = request.formState;
27432740
var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL;
27442741
if (null !== request && "function" === typeof isSignatureEqual) {
2745-
var postbackReferenceId = request[2],
2746-
postbackBoundArity = request[3];
2747-
request[1] === keyJSON &&
2748-
isSignatureEqual.call(
2749-
action,
2750-
postbackReferenceId,
2751-
postbackBoundArity
2752-
) &&
2753-
((formStateMatchingIndex = formStateHookIndex),
2754-
(initialState = request[0]));
2742+
var postbackKey = request[1];
2743+
isSignatureEqual.call(action, request[2], request[3]) &&
2744+
((nextPostbackStateKey =
2745+
void 0 !== permalink
2746+
? "p" + permalink
2747+
: "k" +
2748+
JSON.stringify([componentKeyPath, null, formStateHookIndex])),
2749+
postbackKey === nextPostbackStateKey &&
2750+
((formStateMatchingIndex = formStateHookIndex),
2751+
(initialState = request[0])));
27552752
}
27562753
var boundAction = action.bind(null, initialState);
27572754
action = function (payload) {
@@ -2760,9 +2757,17 @@ function useFormState(action, initialState, permalink) {
27602757
"function" === typeof boundAction.$$FORM_ACTION &&
27612758
(action.$$FORM_ACTION = function (prefix) {
27622759
prefix = boundAction.$$FORM_ACTION(prefix);
2760+
void 0 !== permalink &&
2761+
((permalink += ""), (prefix.action = permalink));
27632762
var formData = prefix.data;
2764-
formData && formData.append("$ACTION_KEY", keyJSON);
2765-
void 0 !== permalink && (prefix.action = permalink + "");
2763+
formData &&
2764+
(null === nextPostbackStateKey &&
2765+
(nextPostbackStateKey =
2766+
void 0 !== permalink
2767+
? "p" + permalink
2768+
: "k" +
2769+
JSON.stringify([componentKeyPath, null, formStateHookIndex])),
2770+
formData.append("$ACTION_KEY", nextPostbackStateKey));
27662771
return prefix;
27672772
});
27682773
return [initialState, action];
@@ -4518,4 +4523,4 @@ exports.renderToString = function (children, options) {
45184523
'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'
45194524
);
45204525
};
4521-
exports.version = "18.3.0-www-modern-8898fe0f";
4526+
exports.version = "18.3.0-www-modern-b31a402c";

0 commit comments

Comments
 (0)