Skip to content

Commit

Permalink
useFormState: MPA submissions to a different page (#27372)
Browse files Browse the repository at this point in the history
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)
  • Loading branch information
acdlite committed Sep 14, 2023
1 parent 1fddce3 commit 97bcd7f
Show file tree
Hide file tree
Showing 8 changed files with 216 additions and 108 deletions.
2 changes: 1 addition & 1 deletion compiled/facebook-www/REVISION
Original file line number Diff line number Diff line change
@@ -1 +1 @@
a6e4791b11816374d015eb4531a82e6cf209c7f2
caa716d50bdeef3a1ac5e3e0cfcc14f4d91f2028
67 changes: 49 additions & 18 deletions compiled/facebook-www/ReactDOMServer-dev.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ if (__DEV__) {
var React = require("react");
var ReactDOM = require("react-dom");

var ReactVersion = "18.3.0-www-classic-f2b91051";
var ReactVersion = "18.3.0-www-classic-360a070f";

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

function createPostbackFormStateKey(permalink, componentKeyPath, hookIndex) {
if (permalink !== undefined) {
return "p" + permalink;
} else {
// Append a node to the key path that represents the form state hook.
var keyPath = [componentKeyPath, null, hookIndex];
return "k" + JSON.stringify(keyPath);
}
}

function useFormState(action, initialState, permalink) {
resolveCurrentlyRenderingComponent(); // Count the number of useFormState hooks per component. We also use this to
// track the position of this useFormState hook relative to the other ones in
Expand All @@ -9440,32 +9450,43 @@ function useFormState(action, initialState, permalink) {
if (typeof formAction === "function") {
// This is a server action. These have additional features to enable
// MPA-style form submissions with progressive enhancement.
// Determine the current form state. If we received state during an MPA form
// TODO: If the same permalink is passed to multiple useFormStates, and
// they all have the same action signature, Fizz will pass the postback
// state to all of them. We should probably only pass it to the first one,
// and/or warn.
// The key is lazily generated and deduped so the that the keypath doesn't
// get JSON.stringify-ed unnecessarily, and at most once.
var nextPostbackStateKey = null; // Determine the current form state. If we received state during an MPA form
// submission, then we will reuse that, if the action identity matches.
// Otherwise we'll use the initial state argument. We will emit a comment
// marker into the stream that indicates whether the state was reused.
var state = initialState; // Append a node to the key path that represents the form state hook.

var componentKey = currentlyRenderingKeyPath;
var key = [componentKey, null, formStateHookIndex];
var keyJSON = JSON.stringify(key);
var state = initialState;
var componentKeyPath = currentlyRenderingKeyPath;
var postbackFormState = getFormState(request); // $FlowIgnore[prop-missing]

var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL;

if (postbackFormState !== null && typeof isSignatureEqual === "function") {
var postbackKeyJSON = postbackFormState[1];
var postbackKey = postbackFormState[1];
var postbackReferenceId = postbackFormState[2];
var postbackBoundArity = postbackFormState[3];

if (
postbackKeyJSON === keyJSON &&
isSignatureEqual.call(action, postbackReferenceId, postbackBoundArity)
) {
// This was a match
formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form.
nextPostbackStateKey = createPostbackFormStateKey(
permalink,
componentKeyPath,
formStateHookIndex
);

state = postbackFormState[0];
if (postbackKey === nextPostbackStateKey) {
// This was a match
formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form.

state = postbackFormState[0];
}
}
} // Bind the state to the first argument of the action.

Expand All @@ -9478,19 +9499,29 @@ function useFormState(action, initialState, permalink) {
if (typeof boundAction.$$FORM_ACTION === "function") {
// $FlowIgnore[prop-missing]
dispatch.$$FORM_ACTION = function (prefix) {
var metadata = boundAction.$$FORM_ACTION(prefix);
var formData = metadata.data;

if (formData) {
formData.append("$ACTION_KEY", keyJSON);
} // Override the action URL
var metadata = boundAction.$$FORM_ACTION(prefix); // Override the action URL

if (permalink !== undefined) {
{
checkAttributeStringCoercion(permalink, "target");
}

metadata.action = permalink + "";
permalink += "";
metadata.action = permalink;
}

var formData = metadata.data;

if (formData) {
if (nextPostbackStateKey === null) {
nextPostbackStateKey = createPostbackFormStateKey(
permalink,
componentKeyPath,
formStateHookIndex
);
}

formData.append("$ACTION_KEY", nextPostbackStateKey);
}

return metadata;
Expand Down
67 changes: 49 additions & 18 deletions compiled/facebook-www/ReactDOMServer-dev.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ if (__DEV__) {
var React = require("react");
var ReactDOM = require("react-dom");

var ReactVersion = "18.3.0-www-modern-82925a4c";
var ReactVersion = "18.3.0-www-modern-b814d906";

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

function createPostbackFormStateKey(permalink, componentKeyPath, hookIndex) {
if (permalink !== undefined) {
return "p" + permalink;
} else {
// Append a node to the key path that represents the form state hook.
var keyPath = [componentKeyPath, null, hookIndex];
return "k" + JSON.stringify(keyPath);
}
}

function useFormState(action, initialState, permalink) {
resolveCurrentlyRenderingComponent(); // Count the number of useFormState hooks per component. We also use this to
// track the position of this useFormState hook relative to the other ones in
Expand All @@ -9199,32 +9209,43 @@ function useFormState(action, initialState, permalink) {
if (typeof formAction === "function") {
// This is a server action. These have additional features to enable
// MPA-style form submissions with progressive enhancement.
// Determine the current form state. If we received state during an MPA form
// TODO: If the same permalink is passed to multiple useFormStates, and
// they all have the same action signature, Fizz will pass the postback
// state to all of them. We should probably only pass it to the first one,
// and/or warn.
// The key is lazily generated and deduped so the that the keypath doesn't
// get JSON.stringify-ed unnecessarily, and at most once.
var nextPostbackStateKey = null; // Determine the current form state. If we received state during an MPA form
// submission, then we will reuse that, if the action identity matches.
// Otherwise we'll use the initial state argument. We will emit a comment
// marker into the stream that indicates whether the state was reused.
var state = initialState; // Append a node to the key path that represents the form state hook.

var componentKey = currentlyRenderingKeyPath;
var key = [componentKey, null, formStateHookIndex];
var keyJSON = JSON.stringify(key);
var state = initialState;
var componentKeyPath = currentlyRenderingKeyPath;
var postbackFormState = getFormState(request); // $FlowIgnore[prop-missing]

var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL;

if (postbackFormState !== null && typeof isSignatureEqual === "function") {
var postbackKeyJSON = postbackFormState[1];
var postbackKey = postbackFormState[1];
var postbackReferenceId = postbackFormState[2];
var postbackBoundArity = postbackFormState[3];

if (
postbackKeyJSON === keyJSON &&
isSignatureEqual.call(action, postbackReferenceId, postbackBoundArity)
) {
// This was a match
formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form.
nextPostbackStateKey = createPostbackFormStateKey(
permalink,
componentKeyPath,
formStateHookIndex
);

state = postbackFormState[0];
if (postbackKey === nextPostbackStateKey) {
// This was a match
formStateMatchingIndex = formStateHookIndex; // Reuse the state that was submitted by the form.

state = postbackFormState[0];
}
}
} // Bind the state to the first argument of the action.

Expand All @@ -9237,19 +9258,29 @@ function useFormState(action, initialState, permalink) {
if (typeof boundAction.$$FORM_ACTION === "function") {
// $FlowIgnore[prop-missing]
dispatch.$$FORM_ACTION = function (prefix) {
var metadata = boundAction.$$FORM_ACTION(prefix);
var formData = metadata.data;

if (formData) {
formData.append("$ACTION_KEY", keyJSON);
} // Override the action URL
var metadata = boundAction.$$FORM_ACTION(prefix); // Override the action URL

if (permalink !== undefined) {
{
checkAttributeStringCoercion(permalink, "target");
}

metadata.action = permalink + "";
permalink += "";
metadata.action = permalink;
}

var formData = metadata.data;

if (formData) {
if (nextPostbackStateKey === null) {
nextPostbackStateKey = createPostbackFormStateKey(
permalink,
componentKeyPath,
formStateHookIndex
);
}

formData.append("$ACTION_KEY", nextPostbackStateKey);
}

return metadata;
Expand Down
41 changes: 23 additions & 18 deletions compiled/facebook-www/ReactDOMServer-prod.classic.js
Original file line number Diff line number Diff line change
Expand Up @@ -2742,24 +2742,21 @@ function useFormState(action, initialState, permalink) {
var formStateHookIndex = formStateCounter++,
request = currentlyRenderingRequest;
if ("function" === typeof action.$$FORM_ACTION) {
var keyJSON = JSON.stringify([
currentlyRenderingKeyPath,
null,
formStateHookIndex
]);
var nextPostbackStateKey = null,
componentKeyPath = currentlyRenderingKeyPath;
request = request.formState;
var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL;
if (null !== request && "function" === typeof isSignatureEqual) {
var postbackReferenceId = request[2],
postbackBoundArity = request[3];
request[1] === keyJSON &&
isSignatureEqual.call(
action,
postbackReferenceId,
postbackBoundArity
) &&
((formStateMatchingIndex = formStateHookIndex),
(initialState = request[0]));
var postbackKey = request[1];
isSignatureEqual.call(action, request[2], request[3]) &&
((nextPostbackStateKey =
void 0 !== permalink
? "p" + permalink
: "k" +
JSON.stringify([componentKeyPath, null, formStateHookIndex])),
postbackKey === nextPostbackStateKey &&
((formStateMatchingIndex = formStateHookIndex),
(initialState = request[0])));
}
var boundAction = action.bind(null, initialState);
action = function (payload) {
Expand All @@ -2768,9 +2765,17 @@ function useFormState(action, initialState, permalink) {
"function" === typeof boundAction.$$FORM_ACTION &&
(action.$$FORM_ACTION = function (prefix) {
prefix = boundAction.$$FORM_ACTION(prefix);
void 0 !== permalink &&
((permalink += ""), (prefix.action = permalink));
var formData = prefix.data;
formData && formData.append("$ACTION_KEY", keyJSON);
void 0 !== permalink && (prefix.action = permalink + "");
formData &&
(null === nextPostbackStateKey &&
(nextPostbackStateKey =
void 0 !== permalink
? "p" + permalink
: "k" +
JSON.stringify([componentKeyPath, null, formStateHookIndex])),
formData.append("$ACTION_KEY", nextPostbackStateKey));
return prefix;
});
return [initialState, action];
Expand Down Expand Up @@ -4551,4 +4556,4 @@ exports.renderToString = function (children, options) {
'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'
);
};
exports.version = "18.3.0-www-classic-f7fe51fc";
exports.version = "18.3.0-www-classic-f04a97ef";
41 changes: 23 additions & 18 deletions compiled/facebook-www/ReactDOMServer-prod.modern.js
Original file line number Diff line number Diff line change
Expand Up @@ -2734,24 +2734,21 @@ function useFormState(action, initialState, permalink) {
var formStateHookIndex = formStateCounter++,
request = currentlyRenderingRequest;
if ("function" === typeof action.$$FORM_ACTION) {
var keyJSON = JSON.stringify([
currentlyRenderingKeyPath,
null,
formStateHookIndex
]);
var nextPostbackStateKey = null,
componentKeyPath = currentlyRenderingKeyPath;
request = request.formState;
var isSignatureEqual = action.$$IS_SIGNATURE_EQUAL;
if (null !== request && "function" === typeof isSignatureEqual) {
var postbackReferenceId = request[2],
postbackBoundArity = request[3];
request[1] === keyJSON &&
isSignatureEqual.call(
action,
postbackReferenceId,
postbackBoundArity
) &&
((formStateMatchingIndex = formStateHookIndex),
(initialState = request[0]));
var postbackKey = request[1];
isSignatureEqual.call(action, request[2], request[3]) &&
((nextPostbackStateKey =
void 0 !== permalink
? "p" + permalink
: "k" +
JSON.stringify([componentKeyPath, null, formStateHookIndex])),
postbackKey === nextPostbackStateKey &&
((formStateMatchingIndex = formStateHookIndex),
(initialState = request[0])));
}
var boundAction = action.bind(null, initialState);
action = function (payload) {
Expand All @@ -2760,9 +2757,17 @@ function useFormState(action, initialState, permalink) {
"function" === typeof boundAction.$$FORM_ACTION &&
(action.$$FORM_ACTION = function (prefix) {
prefix = boundAction.$$FORM_ACTION(prefix);
void 0 !== permalink &&
((permalink += ""), (prefix.action = permalink));
var formData = prefix.data;
formData && formData.append("$ACTION_KEY", keyJSON);
void 0 !== permalink && (prefix.action = permalink + "");
formData &&
(null === nextPostbackStateKey &&
(nextPostbackStateKey =
void 0 !== permalink
? "p" + permalink
: "k" +
JSON.stringify([componentKeyPath, null, formStateHookIndex])),
formData.append("$ACTION_KEY", nextPostbackStateKey));
return prefix;
});
return [initialState, action];
Expand Down Expand Up @@ -4518,4 +4523,4 @@ exports.renderToString = function (children, options) {
'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'
);
};
exports.version = "18.3.0-www-modern-8898fe0f";
exports.version = "18.3.0-www-modern-b31a402c";
Loading

0 comments on commit 97bcd7f

Please sign in to comment.